From 793bf2b99514cdb4d37d0b37d2c0b2a4406ef6ae Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 23 Dec 2024 13:08:01 +0100 Subject: [PATCH 01/53] fix: Empty schema fields in OpenAPI spec (#2045) ## Changes 1. Removes default yaml-fields during schema generation, caused by [this PR](https://github.com/databricks/cli/pull/2032) (current yaml package can't read `json` annotations in struct fields) 2. Addresses missing annotations for fields from OpenAPI spec, which are named differently in go SDK 3. Adds filtering for annotations.yaml to include only CLI package fields 4. Implements alphabetical sort for yaml keys to avoid unnecessary diff in PRs ## Tests Manually tested --- bundle/internal/schema/annotations.go | 62 +- bundle/internal/schema/annotations.yml | 65 +- .../internal/schema/annotations_openapi.yml | 3657 +++++++++-------- bundle/internal/schema/main_test.go | 3 +- bundle/internal/schema/parser.go | 60 +- bundle/schema/jsonschema.json | 19 + 6 files changed, 2097 insertions(+), 1769 deletions(-) diff --git a/bundle/internal/schema/annotations.go b/bundle/internal/schema/annotations.go index aec5e68b0..91aaa4555 100644 --- a/bundle/internal/schema/annotations.go +++ b/bundle/internal/schema/annotations.go @@ -6,6 +6,7 @@ import ( "os" "reflect" "regexp" + "slices" "strings" yaml3 "gopkg.in/yaml.v3" @@ -119,7 +120,15 @@ func (d *annotationHandler) syncWithMissingAnnotations(outputPath string) error if err != nil { return err } - missingAnnotations, err := convert.FromTyped(&d.missingAnnotations, dyn.NilValue) + + for k := range d.missingAnnotations { + if !isCliPath(k) { + delete(d.missingAnnotations, k) + fmt.Printf("Missing annotations for `%s` that are not in CLI package, try to fetch latest OpenAPI spec and regenerate annotations", k) + } + } + + missingAnnotations, err := convert.FromTyped(d.missingAnnotations, dyn.NilValue) if err != nil { return err } @@ -129,7 +138,13 @@ func (d *annotationHandler) syncWithMissingAnnotations(outputPath string) error return err } - err = saveYamlWithStyle(outputPath, output) + var outputTyped annotationFile + err = convert.ToTyped(&outputTyped, output) + if err != nil { + return err + } + + err = saveYamlWithStyle(outputPath, outputTyped) if err != nil { return err } @@ -153,21 +168,50 @@ func assignAnnotation(s *jsonschema.Schema, a annotation) { s.Enum = a.Enum } -func saveYamlWithStyle(outputPath string, input dyn.Value) error { +func saveYamlWithStyle(outputPath string, annotations annotationFile) error { + annotationOrder := yamlsaver.NewOrder([]string{"description", "markdown_description", "title", "default", "enum"}) style := map[string]yaml3.Style{} - file, _ := input.AsMap() - for _, v := range file.Keys() { - style[v.MustString()] = yaml3.LiteralStyle + + order := getAlphabeticalOrder(annotations) + dynMap := map[string]dyn.Value{} + for k, v := range annotations { + style[k] = yaml3.LiteralStyle + + properties := map[string]dyn.Value{} + propertiesOrder := getAlphabeticalOrder(v) + for key, value := range v { + d, err := convert.FromTyped(value, dyn.NilValue) + if d.Kind() == dyn.KindNil || err != nil { + properties[key] = dyn.NewValue(map[string]dyn.Value{}, []dyn.Location{{Line: propertiesOrder.Get(key)}}) + continue + } + val, err := yamlsaver.ConvertToMapValue(value, annotationOrder, []string{}, map[string]dyn.Value{}) + if err != nil { + return err + } + properties[key] = val.WithLocations([]dyn.Location{{Line: propertiesOrder.Get(key)}}) + } + + dynMap[k] = dyn.NewValue(properties, []dyn.Location{{Line: order.Get(k)}}) } saver := yamlsaver.NewSaverWithStyle(style) - err := saver.SaveAsYAML(file, outputPath, true) + err := saver.SaveAsYAML(dynMap, outputPath, true) if err != nil { return err } return nil } +func getAlphabeticalOrder[T any](mapping map[string]T) *yamlsaver.Order { + order := []string{} + for k := range mapping { + order = append(order, k) + } + slices.Sort(order) + return yamlsaver.NewOrder(order) +} + func convertLinksToAbsoluteUrl(s string) string { if s == "" { return s @@ -207,3 +251,7 @@ func convertLinksToAbsoluteUrl(s string) string { return result } + +func isCliPath(path string) bool { + return !strings.HasPrefix(path, "github.com/databricks/databricks-sdk-go") +} diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index e52189daa..84f6753e3 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -417,10 +417,10 @@ github.com/databricks/cli/bundle/config/variable.TargetVariable: "lookup": "description": |- The name of the alert, cluster_policy, cluster, dashboard, instance_pool, job, metastore, pipeline, query, service_principal, or warehouse object for which to retrieve an ID. - "type": + "markdown_description": "description": |- The type of the variable. - "markdown_description": + "type": "description": |- The type of the variable. github.com/databricks/cli/bundle/config/variable.Variable: @@ -438,64 +438,3 @@ github.com/databricks/cli/bundle/config/variable.Variable: "type": "description": |- The type of the variable. -github.com/databricks/databricks-sdk-go/service/serving.Ai21LabsConfig: - "ai21labs_api_key": - "description": |- - PLACEHOLDER - "ai21labs_api_key_plaintext": - "description": |- - PLACEHOLDER -github.com/databricks/databricks-sdk-go/service/serving.GoogleCloudVertexAiConfig: - "private_key": - "description": |- - PLACEHOLDER - "private_key_plaintext": - "description": |- - PLACEHOLDER - "project_id": - "description": |- - PLACEHOLDER - "region": - "description": |- - PLACEHOLDER -github.com/databricks/databricks-sdk-go/service/serving.OpenAiConfig: - "microsoft_entra_client_id": - "description": |- - PLACEHOLDER - "microsoft_entra_client_secret": - "description": |- - PLACEHOLDER - "microsoft_entra_client_secret_plaintext": - "description": |- - PLACEHOLDER - "microsoft_entra_tenant_id": - "description": |- - PLACEHOLDER - "openai_api_base": - "description": |- - PLACEHOLDER - "openai_api_key": - "description": |- - PLACEHOLDER - "openai_api_key_plaintext": - "description": |- - PLACEHOLDER - "openai_api_type": - "description": |- - PLACEHOLDER - "openai_api_version": - "description": |- - PLACEHOLDER - "openai_deployment_name": - "description": |- - PLACEHOLDER - "openai_organization": - "description": |- - PLACEHOLDER -github.com/databricks/databricks-sdk-go/service/serving.PaLmConfig: - "palm_api_key": - "description": |- - PLACEHOLDER - "palm_api_key_plaintext": - "description": |- - PLACEHOLDER diff --git a/bundle/internal/schema/annotations_openapi.yml b/bundle/internal/schema/annotations_openapi.yml index d8f681a28..e9c893c87 100644 --- a/bundle/internal/schema/annotations_openapi.yml +++ b/bundle/internal/schema/annotations_openapi.yml @@ -1,89 +1,89 @@ # This file is auto-generated. DO NOT EDIT. github.com/databricks/cli/bundle/config/resources.Cluster: - 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. - autoscale: - description: |- + "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. + "autoscale": + "description": |- Parameters needed in order to automatically scale clusters up and down based on load. Note: autoscaling works best with DB runtime versions 3.0 or later. - autotermination_minutes: - description: |- + "autotermination_minutes": + "description": |- Automatically terminates the cluster after it is inactive for this time in minutes. If not set, this cluster will not be automatically terminated. If specified, the threshold must be between 10 and 10000 minutes. Users can also set this value to 0 to explicitly disable automatic termination. - aws_attributes: - description: |- + "aws_attributes": + "description": |- Attributes related to clusters running on Amazon Web Services. If not specified at cluster creation, a set of default values will be used. - azure_attributes: - description: |- + "azure_attributes": + "description": |- Attributes related to clusters running on Microsoft Azure. If not specified at cluster creation, a set of default values will be used. - cluster_log_conf: - description: |- + "cluster_log_conf": + "description": |- The configuration for delivering spark logs to a long-term storage destination. Two kinds of destinations (dbfs and s3) are supported. Only one destination can be specified for one cluster. If the conf is given, the logs will be delivered to the destination every `5 mins`. The destination of driver logs is `$destination/$clusterId/driver`, while the destination of executor logs is `$destination/$clusterId/executor`. - cluster_name: - description: | + "cluster_name": + "description": | Cluster name requested by the user. This doesn't have to be unique. If not specified at creation, the cluster name will be an empty string. - custom_tags: - description: |- + "custom_tags": + "description": |- Additional tags for cluster resources. Databricks will tag all cluster resources (e.g., AWS instances and EBS volumes) with these tags in addition to `default_tags`. Notes: - Currently, Databricks allows at most 45 custom tags - Clusters can only reuse cloud resources if the resources' tags are a subset of the cluster tags - data_security_mode: {} - docker_image: {} - driver_instance_pool_id: - description: |- + "data_security_mode": {} + "docker_image": {} + "driver_instance_pool_id": + "description": |- The optional ID of the instance pool for the driver of the cluster belongs. The pool cluster uses the instance pool with id (instance_pool_id) if the driver pool is not assigned. - driver_node_type_id: - description: | + "driver_node_type_id": + "description": | The node type of the Spark driver. Note that this field is optional; if unset, the driver node type will be set as the same value as `node_type_id` defined above. - enable_elastic_disk: - description: |- + "enable_elastic_disk": + "description": |- Autoscaling Local Storage: when enabled, this cluster will dynamically acquire additional disk space when its Spark workers are running low on disk space. This feature requires specific AWS permissions to function correctly - refer to the User Guide for more details. - enable_local_disk_encryption: - description: Whether to enable LUKS on cluster VMs' local disks - gcp_attributes: - description: |- + "enable_local_disk_encryption": + "description": |- + Whether to enable LUKS on cluster VMs' local disks + "gcp_attributes": + "description": |- Attributes related to clusters running on Google Cloud Platform. If not specified at cluster creation, a set of default values will be used. - 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 `//init_scripts`. - instance_pool_id: - description: The optional ID of the instance pool to which the cluster belongs. - is_single_node: - description: | + "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 `//init_scripts`. + "instance_pool_id": + "description": |- + The optional ID of the instance pool to which the cluster belongs. + "is_single_node": + "description": | This field can only be used with `kind`. When set to true, Databricks will automatically set single node related `custom_tags`, `spark_conf`, and `num_workers` - kind: {} - node_type_id: - description: | + "kind": {} + "node_type_id": + "description": | This field encodes, through a single value, the resources available to each of the Spark nodes in this cluster. For example, the Spark nodes can be provisioned and optimized for memory or compute intensive workloads. A list of available node types can be retrieved by using the :method:clusters/listNodeTypes API call. - num_workers: - description: |- + "num_workers": + "description": |- Number of worker nodes that this cluster should have. A cluster has one Spark Driver and `num_workers` Executors for a total of `num_workers` + 1 Spark nodes. @@ -92,18 +92,20 @@ github.com/databricks/cli/bundle/config/resources.Cluster: is resized from 5 to 10 workers, this field will immediately be updated to reflect the target size of 10 workers, whereas the workers listed in `spark_info` will gradually increase from 5 to 10 as the new nodes are provisioned. - policy_id: - description: The ID of the cluster policy used to create the cluster if applicable. - runtime_engine: {} - single_user_name: - description: Single user name if data_security_mode is `SINGLE_USER` - spark_conf: - description: | + "policy_id": + "description": |- + The ID of the cluster policy used to create the cluster if applicable. + "runtime_engine": {} + "single_user_name": + "description": |- + Single user name if data_security_mode is `SINGLE_USER` + "spark_conf": + "description": | An object containing a set of optional, user-specified Spark configuration key-value pairs. Users can also pass in a string of extra JVM options to the driver and the executors via `spark.driver.extraJavaOptions` and `spark.executor.extraJavaOptions` respectively. - spark_env_vars: - description: |- + "spark_env_vars": + "description": |- An object containing a set of optional, user-specified environment variable key-value pairs. Please note that key-value pair of the form (X,Y) will be exported as is (i.e., `export X='Y'`) while launching the driver and workers. @@ -115,399 +117,460 @@ github.com/databricks/cli/bundle/config/resources.Cluster: Example Spark environment variables: `{"SPARK_WORKER_MEMORY": "28000m", "SPARK_LOCAL_DIRS": "/local_disk0"}` or `{"SPARK_DAEMON_JAVA_OPTS": "$SPARK_DAEMON_JAVA_OPTS -Dspark.shuffle.service.enabled=true"}` - spark_version: - description: | + "spark_version": + "description": | The Spark version of the cluster, e.g. `3.3.x-scala2.11`. A list of available Spark versions can be retrieved by using the :method:clusters/sparkVersions API call. - ssh_public_keys: - description: |- + "ssh_public_keys": + "description": |- SSH public key contents that will be added to each Spark node in this cluster. The corresponding private keys can be used to login with the user name `ubuntu` on port `2200`. Up to 10 keys can be specified. - use_ml_runtime: - description: | + "use_ml_runtime": + "description": | This field can only be used with `kind`. `effective_spark_version` is determined by `spark_version` (DBR release), this field `use_ml_runtime`, and whether `node_type_id` is gpu node or not. - workload_type: {} + "workload_type": {} github.com/databricks/cli/bundle/config/resources.Dashboard: - create_time: - description: The timestamp of when the dashboard was created. - dashboard_id: - description: UUID identifying the dashboard. - display_name: - description: The display name of the dashboard. - etag: - description: |- + "create_time": + "description": |- + The timestamp of when the dashboard was created. + "dashboard_id": + "description": |- + UUID identifying the dashboard. + "display_name": + "description": |- + The display name of the dashboard. + "etag": + "description": |- The etag for the dashboard. Can be optionally provided on updates to ensure that the dashboard has not been modified since the last read. This field is excluded in List Dashboards responses. - lifecycle_state: - description: The state of the dashboard resource. Used for tracking trashed status. - parent_path: - description: |- + "lifecycle_state": + "description": |- + The state of the dashboard resource. Used for tracking trashed status. + "parent_path": + "description": |- The workspace path of the folder containing the dashboard. Includes leading slash and no trailing slash. This field is excluded in List Dashboards responses. - path: - description: |- + "path": + "description": |- The workspace path of the dashboard asset, including the file name. Exported dashboards always have the file extension `.lvdash.json`. This field is excluded in List Dashboards responses. - serialized_dashboard: - description: |- + "serialized_dashboard": + "description": |- The contents of the dashboard in serialized string form. This field is excluded in List Dashboards responses. Use the [get dashboard API](https://docs.databricks.com/api/workspace/lakeview/get) to retrieve an example response, which includes the `serialized_dashboard` field. This field provides the structure of the JSON string that represents the dashboard's layout and components. - update_time: - description: |- + "update_time": + "description": |- The timestamp of when the dashboard was last updated by the user. This field is excluded in List Dashboards responses. - warehouse_id: - description: The warehouse ID used to run the dashboard. + "warehouse_id": + "description": |- + The warehouse ID used to run the dashboard. github.com/databricks/cli/bundle/config/resources.Job: - budget_policy_id: - description: |- + "budget_policy_id": + "description": |- The id of the user specified budget policy to use for this job. If not specified, a default budget policy may be applied when creating or modifying the job. See `effective_budget_policy_id` for the budget policy used by this workload. - continuous: - 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. - deployment: - description: Deployment information for jobs managed by external sources. - description: - description: An optional description for the job. The maximum length is 27700 - characters in UTF-8 encoding. - edit_mode: - description: |- + "continuous": + "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. + "deployment": + "description": |- + Deployment information for jobs managed by external sources. + "description": + "description": |- + An optional description for the job. The maximum length is 27700 characters in UTF-8 encoding. + "edit_mode": + "description": |- Edit mode of the job. * `UI_LOCKED`: The job is in a locked UI state and cannot be modified. * `EDITABLE`: The job is in an editable state and can be modified. - 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. - environments: - description: |- + "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. + "environments": + "description": |- A list of task execution environment specifications that can be referenced by serverless tasks of this job. An environment is required to be present for serverless tasks. For serverless notebook tasks, the environment is accessible in the notebook environment panel. For other serverless tasks, the task environment is required to be specified using environment_key in the task settings. - 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"`. - git_source: - description: |- + "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"`. + "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. If `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. Note: 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. - health: {} - job_clusters: - description: A list of job cluster specifications that can be shared and reused - by tasks of this job. Libraries cannot be declared in a shared job cluster. - You must declare dependent libraries in task settings. - max_concurrent_runs: - description: |- + "health": {} + "job_clusters": + "description": |- + A list of job cluster specifications that can be shared and reused by tasks of this job. Libraries cannot be declared in a shared job cluster. You must declare dependent libraries in task settings. + "max_concurrent_runs": + "description": |- An optional maximum allowed number of concurrent runs of the job. Set this value if you want to be able to execute multiple runs of the same job concurrently. This is useful for example if you trigger your job on a frequent schedule and want to allow consecutive runs to overlap with each other, or if you want to trigger multiple runs which differ by their input parameters. This setting affects only new runs. For example, suppose the job’s concurrency is 4 and there are 4 concurrent active runs. Then setting the concurrency to 3 won’t kill any of the active runs. However, from then on, new runs are skipped unless there are fewer than 3 active runs. This value cannot exceed 1000. Setting this value to `0` causes all new runs to be skipped. - name: - description: An optional name for the job. The maximum length is 4096 bytes in - UTF-8 encoding. - notification_settings: - description: Optional notification settings that are used when sending notifications - to each of the `email_notifications` and `webhook_notifications` for this job. - parameters: - description: Job-level parameter definitions - queue: - description: The queue settings of the job. - run_as: {} - schedule: - description: An optional periodic schedule for this job. The default behavior - is that the job only runs when triggered by clicking “Run Now” in the Jobs UI - or sending an API request to `runNow`. - tags: - description: A map of tags associated with the job. These are forwarded to the - cluster as cluster tags for jobs clusters, and are subject to the same limitations - as cluster tags. A maximum of 25 tags can be added to the job. - tasks: - description: A list of task specifications to be executed by this job. - timeout_seconds: - description: An optional timeout applied to each run of this job. A value of `0` - means no timeout. - trigger: - description: A configuration to trigger a run when certain conditions are met. - The default behavior is that the job runs only when triggered by clicking “Run - Now” in the Jobs UI or sending an API request to `runNow`. - webhook_notifications: - description: A collection of system notification IDs to notify when runs of this - job begin or complete. + "name": + "description": |- + An optional name for the job. The maximum length is 4096 bytes in UTF-8 encoding. + "notification_settings": + "description": |- + Optional notification settings that are used when sending notifications to each of the `email_notifications` and `webhook_notifications` for this job. + "parameters": + "description": |- + Job-level parameter definitions + "queue": + "description": |- + The queue settings of the job. + "run_as": {} + "schedule": + "description": |- + An optional periodic schedule for this job. The default behavior is that the job only runs when triggered by clicking “Run Now” in the Jobs UI or sending an API request to `runNow`. + "tags": + "description": |- + A map of tags associated with the job. These are forwarded to the cluster as cluster tags for jobs clusters, and are subject to the same limitations as cluster tags. A maximum of 25 tags can be added to the job. + "tasks": + "description": |- + A list of task specifications to be executed by this job. + "timeout_seconds": + "description": |- + An optional timeout applied to each run of this job. A value of `0` means no timeout. + "trigger": + "description": |- + A configuration to trigger a run when certain conditions are met. The default behavior is that the job runs only when triggered by clicking “Run Now” in the Jobs UI or sending an API request to `runNow`. + "webhook_notifications": + "description": |- + A collection of system notification IDs to notify when runs of this job begin or complete. github.com/databricks/cli/bundle/config/resources.MlflowExperiment: - artifact_location: - description: Location where artifacts for the experiment are stored. - creation_time: - description: Creation time - experiment_id: - description: Unique identifier for the experiment. - last_update_time: - description: Last update time - lifecycle_stage: - description: |- + "artifact_location": + "description": |- + Location where artifacts for the experiment are stored. + "creation_time": + "description": |- + Creation time + "experiment_id": + "description": |- + Unique identifier for the experiment. + "last_update_time": + "description": |- + Last update time + "lifecycle_stage": + "description": |- Current life cycle stage of the experiment: "active" or "deleted". Deleted experiments are not returned by APIs. - name: - description: Human readable name that identifies the experiment. - tags: - description: 'Tags: Additional metadata key-value pairs.' + "name": + "description": |- + Human readable name that identifies the experiment. + "tags": + "description": |- + Tags: Additional metadata key-value pairs. github.com/databricks/cli/bundle/config/resources.MlflowModel: - creation_timestamp: - description: Timestamp recorded when this `registered_model` was created. - description: - description: Description of this `registered_model`. - last_updated_timestamp: - description: Timestamp recorded when metadata for this `registered_model` was - last updated. - latest_versions: - description: |- + "creation_timestamp": + "description": |- + Timestamp recorded when this `registered_model` was created. + "description": + "description": |- + Description of this `registered_model`. + "last_updated_timestamp": + "description": |- + Timestamp recorded when metadata for this `registered_model` was last updated. + "latest_versions": + "description": |- Collection of latest model versions for each stage. Only contains models with current `READY` status. - name: - description: Unique name for the model. - tags: - description: 'Tags: Additional metadata key-value pairs for this `registered_model`.' - user_id: - description: User that created this `registered_model` + "name": + "description": |- + Unique name for the model. + "tags": + "description": |- + Tags: Additional metadata key-value pairs for this `registered_model`. + "user_id": + "description": |- + User that created this `registered_model` github.com/databricks/cli/bundle/config/resources.ModelServingEndpoint: - ai_gateway: - description: 'The AI Gateway configuration for the serving endpoint. NOTE: only - external model endpoints are supported as of now.' - config: - description: The core config of the serving endpoint. - name: - description: | + "ai_gateway": + "description": |- + The AI Gateway configuration for the serving endpoint. NOTE: only external model endpoints are supported as of now. + "config": + "description": |- + The core config of the serving endpoint. + "name": + "description": | The name of the serving endpoint. This field is required and must be unique across a Databricks workspace. An endpoint name can consist of alphanumeric characters, dashes, and underscores. - rate_limits: - description: 'Rate limits to be applied to the serving endpoint. NOTE: this field - is deprecated, please use AI Gateway to manage rate limits.' - route_optimized: - description: Enable route optimization for the serving endpoint. - tags: - description: Tags to be attached to the serving endpoint and automatically propagated - to billing logs. + "rate_limits": + "description": |- + Rate limits to be applied to the serving endpoint. NOTE: this field is deprecated, please use AI Gateway to manage rate limits. + "route_optimized": + "description": |- + Enable route optimization for the serving endpoint. + "tags": + "description": |- + Tags to be attached to the serving endpoint and automatically propagated to billing logs. github.com/databricks/cli/bundle/config/resources.Pipeline: - budget_policy_id: - description: Budget policy of this pipeline. - 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. - channel: - description: DLT Release Channel that specifies which version to use. - clusters: - description: Cluster settings for this pipeline deployment. - configuration: - description: String-String configuration for this pipeline execution. - continuous: - description: Whether the pipeline is continuous or triggered. This replaces `trigger`. - deployment: - description: Deployment type of this pipeline. - development: - description: Whether the pipeline is in Development mode. Defaults to false. - edition: - description: Pipeline product edition. - filters: - description: Filters on which Pipeline packages to include in the deployed graph. - gateway_definition: - description: The definition of a gateway pipeline to support change data capture. - id: - description: Unique identifier for this pipeline. - ingestion_definition: - description: The configuration for a managed ingestion pipeline. These settings - cannot be used with the 'libraries', 'target' or 'catalog' settings. - libraries: - description: Libraries or code needed by this deployment. - name: - description: Friendly identifier for this pipeline. - notifications: - description: List of notification settings for this pipeline. - photon: - description: Whether Photon is enabled for this pipeline. - restart_window: - description: Restart window of this pipeline. - 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. - serverless: - description: Whether serverless compute is enabled for this pipeline. - storage: - description: DBFS root directory for storing checkpoints and tables. - target: - description: Target schema (database) to add tables in this pipeline to. If not - specified, no data is published to the Hive metastore or Unity Catalog. To publish - to Unity Catalog, also specify `catalog`. - trigger: - description: 'Which pipeline trigger to use. Deprecated: Use `continuous` instead.' + "budget_policy_id": + "description": |- + Budget policy of this pipeline. + "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. + "channel": + "description": |- + DLT Release Channel that specifies which version to use. + "clusters": + "description": |- + Cluster settings for this pipeline deployment. + "configuration": + "description": |- + String-String configuration for this pipeline execution. + "continuous": + "description": |- + Whether the pipeline is continuous or triggered. This replaces `trigger`. + "deployment": + "description": |- + Deployment type of this pipeline. + "development": + "description": |- + Whether the pipeline is in Development mode. Defaults to false. + "edition": + "description": |- + Pipeline product edition. + "filters": + "description": |- + Filters on which Pipeline packages to include in the deployed graph. + "gateway_definition": + "description": |- + The definition of a gateway pipeline to support change data capture. + "id": + "description": |- + Unique identifier for this pipeline. + "ingestion_definition": + "description": |- + The configuration for a managed ingestion pipeline. These settings cannot be used with the 'libraries', 'target' or 'catalog' settings. + "libraries": + "description": |- + Libraries or code needed by this deployment. + "name": + "description": |- + Friendly identifier for this pipeline. + "notifications": + "description": |- + List of notification settings for this pipeline. + "photon": + "description": |- + Whether Photon is enabled for this pipeline. + "restart_window": + "description": |- + Restart window of this pipeline. + "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. + "serverless": + "description": |- + Whether serverless compute is enabled for this pipeline. + "storage": + "description": |- + DBFS root directory for storing checkpoints and tables. + "target": + "description": |- + Target schema (database) to add tables in this pipeline to. If not specified, no data is published to the Hive metastore or Unity Catalog. To publish to Unity Catalog, also specify `catalog`. + "trigger": + "description": |- + Which pipeline trigger to use. Deprecated: Use `continuous` instead. github.com/databricks/cli/bundle/config/resources.QualityMonitor: - assets_dir: - description: The directory to store monitoring assets (e.g. dashboard, metric - tables). - baseline_table_name: - description: | + "assets_dir": + "description": |- + The directory to store monitoring assets (e.g. dashboard, metric tables). + "baseline_table_name": + "description": | Name of the baseline table from which drift metrics are computed from. Columns in the monitored table should also be present in the baseline table. - custom_metrics: - description: | + "custom_metrics": + "description": | Custom metrics to compute on the monitored table. These can be aggregate metrics, derived metrics (from already computed aggregate metrics), or drift metrics (comparing metrics across time windows). - data_classification_config: - description: The data classification config for the monitor. - inference_log: - description: Configuration for monitoring inference logs. - notifications: - description: The notification settings for the monitor. - output_schema_name: - description: Schema where output metric tables are created. - schedule: - description: The schedule for automatically updating and refreshing metric tables. - skip_builtin_dashboard: - description: Whether to skip creating a default dashboard summarizing data quality - metrics. - slicing_exprs: - description: | + "data_classification_config": + "description": |- + The data classification config for the monitor. + "inference_log": + "description": |- + Configuration for monitoring inference logs. + "notifications": + "description": |- + The notification settings for the monitor. + "output_schema_name": + "description": |- + Schema where output metric tables are created. + "schedule": + "description": |- + The schedule for automatically updating and refreshing metric tables. + "skip_builtin_dashboard": + "description": |- + Whether to skip creating a default dashboard summarizing data quality metrics. + "slicing_exprs": + "description": | List of column expressions to slice data with for targeted analysis. The data is grouped by each expression independently, resulting in a separate slice for each predicate and its complements. For high-cardinality columns, only the top 100 unique values by frequency will generate slices. - snapshot: - description: Configuration for monitoring snapshot tables. - time_series: - description: Configuration for monitoring time series tables. - warehouse_id: - description: | + "snapshot": + "description": |- + Configuration for monitoring snapshot tables. + "time_series": + "description": |- + Configuration for monitoring time series tables. + "warehouse_id": + "description": | Optional argument to specify the warehouse for dashboard creation. If not specified, the first running warehouse will be used. github.com/databricks/cli/bundle/config/resources.RegisteredModel: - catalog_name: - description: The name of the catalog where the schema and the registered model - reside - comment: - description: The comment attached to the registered model - name: - description: The name of the registered model - schema_name: - description: The name of the schema where the registered model resides - storage_location: - description: The storage location on the cloud under which model version data - files are stored + "catalog_name": + "description": |- + The name of the catalog where the schema and the registered model reside + "comment": + "description": |- + The comment attached to the registered model + "name": + "description": |- + The name of the registered model + "schema_name": + "description": |- + The name of the schema where the registered model resides + "storage_location": + "description": |- + The storage location on the cloud under which model version data files are stored github.com/databricks/cli/bundle/config/resources.Schema: - catalog_name: - description: Name of parent catalog. - comment: - description: User-provided free-form text description. - name: - description: Name of schema, relative to parent catalog. - properties: {} - storage_root: - description: Storage root URL for managed tables within schema. + "catalog_name": + "description": |- + Name of parent catalog. + "comment": + "description": |- + User-provided free-form text description. + "name": + "description": |- + Name of schema, relative to parent catalog. + "properties": {} + "storage_root": + "description": |- + Storage root URL for managed tables within schema. github.com/databricks/cli/bundle/config/resources.Volume: - catalog_name: - description: The name of the catalog where the schema and the volume are - comment: - description: The comment attached to the volume - name: - description: The name of the volume - schema_name: - description: The name of the schema where the volume is - storage_location: - description: The storage location on the cloud - volume_type: {} + "catalog_name": + "description": |- + The name of the catalog where the schema and the volume are + "comment": + "description": |- + The comment attached to the volume + "name": + "description": |- + The name of the volume + "schema_name": + "description": |- + The name of the schema where the volume is + "storage_location": + "description": |- + The storage location on the cloud + "volume_type": {} github.com/databricks/databricks-sdk-go/service/catalog.MonitorCronSchedule: - pause_status: - description: Read only field that indicates whether a schedule is paused or not. - quartz_cron_expression: - description: | + "pause_status": + "description": |- + Read only field that indicates whether a schedule is paused or not. + "quartz_cron_expression": + "description": | The expression that determines when to run the monitor. See [examples](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html). - timezone_id: - description: | + "timezone_id": + "description": | The timezone id (e.g., ``"PST"``) in which to evaluate the quartz expression. github.com/databricks/databricks-sdk-go/service/catalog.MonitorCronSchedulePauseStatus: - _: - description: Read only field that indicates whether a schedule is paused or not. - enum: - - UNPAUSED - - PAUSED + "_": + "description": |- + Read only field that indicates whether a schedule is paused or not. + "enum": + - |- + UNPAUSED + - |- + PAUSED github.com/databricks/databricks-sdk-go/service/catalog.MonitorDataClassificationConfig: - enabled: - description: Whether data classification is enabled. + "enabled": + "description": |- + Whether data classification is enabled. github.com/databricks/databricks-sdk-go/service/catalog.MonitorDestination: - email_addresses: - description: The list of email addresses to send the notification to. A maximum - of 5 email addresses is supported. + "email_addresses": + "description": |- + The list of email addresses to send the notification to. A maximum of 5 email addresses is supported. github.com/databricks/databricks-sdk-go/service/catalog.MonitorInferenceLog: - granularities: - description: | + "granularities": + "description": | Granularities for aggregating data into time windows based on their timestamp. Currently the following static granularities are supported: {``"5 minutes"``, ``"30 minutes"``, ``"1 hour"``, ``"1 day"``, ``" week(s)"``, ``"1 month"``, ``"1 year"``}. - label_col: - description: Optional column that contains the ground truth for the prediction. - model_id_col: - description: | + "label_col": + "description": |- + Optional column that contains the ground truth for the prediction. + "model_id_col": + "description": | Column that contains the id of the model generating the predictions. Metrics will be computed per model id by default, and also across all model ids. - prediction_col: - description: Column that contains the output/prediction from the model. - prediction_proba_col: - description: | + "prediction_col": + "description": |- + Column that contains the output/prediction from the model. + "prediction_proba_col": + "description": | Optional column that contains the prediction probabilities for each class in a classification problem type. The values in this column should be a map, mapping each class label to the prediction probability for a given sample. The map should be of PySpark MapType(). - problem_type: - description: Problem type the model aims to solve. Determines the type of model-quality - metrics that will be computed. - timestamp_col: - description: | + "problem_type": + "description": |- + Problem type the model aims to solve. Determines the type of model-quality metrics that will be computed. + "timestamp_col": + "description": | Column that contains the timestamps of requests. The column must be one of the following: - A ``TimestampType`` column - A column whose values can be converted to timestamps through the pyspark ``to_timestamp`` [function](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.functions.to_timestamp.html). github.com/databricks/databricks-sdk-go/service/catalog.MonitorInferenceLogProblemType: - _: - description: Problem type the model aims to solve. Determines the type of model-quality - metrics that will be computed. - enum: - - PROBLEM_TYPE_CLASSIFICATION - - PROBLEM_TYPE_REGRESSION + "_": + "description": |- + Problem type the model aims to solve. Determines the type of model-quality metrics that will be computed. + "enum": + - |- + PROBLEM_TYPE_CLASSIFICATION + - |- + PROBLEM_TYPE_REGRESSION github.com/databricks/databricks-sdk-go/service/catalog.MonitorMetric: - definition: - description: Jinja template for a SQL expression that specifies how to compute - the metric. See [create metric definition](https://docs.databricks.com/en/lakehouse-monitoring/custom-metrics.html#create-definition). - input_columns: - description: | + "definition": + "description": |- + Jinja template for a SQL expression that specifies how to compute the metric. See [create metric definition](https://docs.databricks.com/en/lakehouse-monitoring/custom-metrics.html#create-definition). + "input_columns": + "description": | A list of column names in the input table the metric should be computed for. Can use ``":table"`` to indicate that the metric needs information from multiple columns. - name: - description: Name of the metric in the output tables. - output_data_type: - description: The output type of the custom metric. - type: - description: | + "name": + "description": |- + Name of the metric in the output tables. + "output_data_type": + "description": |- + The output type of the custom metric. + "type": + "description": | Can only be one of ``"CUSTOM_METRIC_TYPE_AGGREGATE"``, ``"CUSTOM_METRIC_TYPE_DERIVED"``, or ``"CUSTOM_METRIC_TYPE_DRIFT"``. The ``"CUSTOM_METRIC_TYPE_AGGREGATE"`` and ``"CUSTOM_METRIC_TYPE_DERIVED"`` metrics are computed on a single table, whereas the ``"CUSTOM_METRIC_TYPE_DRIFT"`` compare metrics across @@ -516,8 +579,8 @@ github.com/databricks/databricks-sdk-go/service/catalog.MonitorMetric: - CUSTOM_METRIC_TYPE_DERIVED: depend on previously computed aggregate metrics - CUSTOM_METRIC_TYPE_DRIFT: depend on previously computed aggregate or derived metrics github.com/databricks/databricks-sdk-go/service/catalog.MonitorMetricType: - _: - description: | + "_": + "description": | Can only be one of ``"CUSTOM_METRIC_TYPE_AGGREGATE"``, ``"CUSTOM_METRIC_TYPE_DERIVED"``, or ``"CUSTOM_METRIC_TYPE_DRIFT"``. The ``"CUSTOM_METRIC_TYPE_AGGREGATE"`` and ``"CUSTOM_METRIC_TYPE_DERIVED"`` metrics are computed on a single table, whereas the ``"CUSTOM_METRIC_TYPE_DRIFT"`` compare metrics across @@ -525,50 +588,57 @@ github.com/databricks/databricks-sdk-go/service/catalog.MonitorMetricType: - CUSTOM_METRIC_TYPE_AGGREGATE: only depend on the existing columns in your table - CUSTOM_METRIC_TYPE_DERIVED: depend on previously computed aggregate metrics - CUSTOM_METRIC_TYPE_DRIFT: depend on previously computed aggregate or derived metrics - enum: - - CUSTOM_METRIC_TYPE_AGGREGATE - - CUSTOM_METRIC_TYPE_DERIVED - - CUSTOM_METRIC_TYPE_DRIFT + "enum": + - |- + CUSTOM_METRIC_TYPE_AGGREGATE + - |- + CUSTOM_METRIC_TYPE_DERIVED + - |- + CUSTOM_METRIC_TYPE_DRIFT github.com/databricks/databricks-sdk-go/service/catalog.MonitorNotifications: - on_failure: - description: Who to send notifications to on monitor failure. - on_new_classification_tag_detected: - description: Who to send notifications to when new data classification tags are - detected. + "on_failure": + "description": |- + Who to send notifications to on monitor failure. + "on_new_classification_tag_detected": + "description": |- + Who to send notifications to when new data classification tags are detected. github.com/databricks/databricks-sdk-go/service/catalog.MonitorSnapshot: {} github.com/databricks/databricks-sdk-go/service/catalog.MonitorTimeSeries: - granularities: - description: | + "granularities": + "description": | Granularities for aggregating data into time windows based on their timestamp. Currently the following static granularities are supported: {``"5 minutes"``, ``"30 minutes"``, ``"1 hour"``, ``"1 day"``, ``" week(s)"``, ``"1 month"``, ``"1 year"``}. - timestamp_col: - description: | + "timestamp_col": + "description": | Column that contains the timestamps of requests. The column must be one of the following: - A ``TimestampType`` column - A column whose values can be converted to timestamps through the pyspark ``to_timestamp`` [function](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.functions.to_timestamp.html). github.com/databricks/databricks-sdk-go/service/catalog.VolumeType: - _: - enum: - - EXTERNAL - - MANAGED + "_": + "enum": + - |- + EXTERNAL + - |- + MANAGED github.com/databricks/databricks-sdk-go/service/compute.Adlsgen2Info: - destination: - description: abfss destination, e.g. `abfss://@.dfs.core.windows.net/`. + "destination": + "description": |- + abfss destination, e.g. `abfss://@.dfs.core.windows.net/`. github.com/databricks/databricks-sdk-go/service/compute.AutoScale: - max_workers: - description: |- + "max_workers": + "description": |- The maximum number of workers to which the cluster can scale up when overloaded. Note that `max_workers` must be strictly greater than `min_workers`. - min_workers: - description: |- + "min_workers": + "description": |- The minimum number of workers to which the cluster can scale down when underutilized. It is also the initial number of workers the cluster will have after creation. github.com/databricks/databricks-sdk-go/service/compute.AwsAttributes: - availability: {} - ebs_volume_count: - description: |- + "availability": {} + "ebs_volume_count": + "description": |- The number of volumes launched for each instance. Users can choose up to 10 volumes. This feature is only enabled for supported node types. Legacy node types cannot specify custom EBS volumes. @@ -585,22 +655,20 @@ github.com/databricks/databricks-sdk-go/service/compute.AwsAttributes: Please note that if EBS volumes are specified, then the Spark configuration `spark.local.dir` will be overridden. - ebs_volume_iops: - description: If using gp3 volumes, what IOPS to use for the disk. If this is not - set, the maximum performance of a gp2 volume with the same volume size will - be used. - ebs_volume_size: - description: |- + "ebs_volume_iops": + "description": |- + If using gp3 volumes, what IOPS to use for the disk. If this is not set, the maximum performance of a gp2 volume with the same volume size will be used. + "ebs_volume_size": + "description": |- The size of each EBS volume (in GiB) launched for each instance. For general purpose SSD, this value must be within the range 100 - 4096. For throughput optimized HDD, this value must be within the range 500 - 4096. - ebs_volume_throughput: - description: If using gp3 volumes, what throughput to use for the disk. If this - is not set, the maximum performance of a gp2 volume with the same volume size - will be used. - ebs_volume_type: {} - first_on_demand: - description: |- + "ebs_volume_throughput": + "description": |- + If using gp3 volumes, what throughput to use for the disk. If this is not set, the maximum performance of a gp2 volume with the same volume size will be used. + "ebs_volume_type": {} + "first_on_demand": + "description": |- The first `first_on_demand` nodes of the cluster will be placed on on-demand instances. If this value is greater than 0, the cluster driver node in particular will be placed on an on-demand instance. If this value is greater than or equal to the current cluster size, all @@ -608,8 +676,8 @@ github.com/databricks/databricks-sdk-go/service/compute.AwsAttributes: size, `first_on_demand` nodes will be placed on on-demand instances and the remainder will be placed on `availability` instances. Note that this value does not affect cluster size and cannot currently be mutated over the lifetime of a cluster. - instance_profile_arn: - description: |- + "instance_profile_arn": + "description": |- Nodes for this cluster will only be placed on AWS instances with this instance profile. If ommitted, nodes will be placed on instances without an IAM instance profile. The instance profile must have previously been added to the Databricks environment by an account @@ -618,8 +686,8 @@ github.com/databricks/databricks-sdk-go/service/compute.AwsAttributes: This feature may only be available to certain customer plans. If this field is ommitted, we will pull in the default from the conf if it exists. - spot_bid_price_percent: - description: |- + "spot_bid_price_percent": + "description": |- The bid price for AWS spot instances, as a percentage of the corresponding instance type's on-demand price. For example, if this field is set to 50, and the cluster needs a new `r3.xlarge` spot @@ -632,8 +700,8 @@ github.com/databricks/databricks-sdk-go/service/compute.AwsAttributes: The default value and documentation here should be kept consistent with CommonConf.defaultSpotBidPricePercent and CommonConf.maxSpotBidPricePercent. - zone_id: - description: |- + "zone_id": + "description": |- Identifier for the availability zone/datacenter in which the cluster resides. This string will be of a form like "us-west-2a". The provided availability zone must be in the same region as the Databricks deployment. For example, "us-west-2a" @@ -644,19 +712,22 @@ github.com/databricks/databricks-sdk-go/service/compute.AwsAttributes: The list of available zones as well as the default value can be found by using the `List Zones` method. github.com/databricks/databricks-sdk-go/service/compute.AwsAvailability: - _: - description: | + "_": + "description": | Availability type used for all subsequent nodes past the `first_on_demand` ones. Note: If `first_on_demand` is zero, this availability type will be used for the entire cluster. - enum: - - SPOT - - ON_DEMAND - - SPOT_WITH_FALLBACK + "enum": + - |- + SPOT + - |- + ON_DEMAND + - |- + SPOT_WITH_FALLBACK github.com/databricks/databricks-sdk-go/service/compute.AzureAttributes: - availability: {} - first_on_demand: - description: |- + "availability": {} + "first_on_demand": + "description": |- The first `first_on_demand` nodes of the cluster will be placed on on-demand instances. This value should be greater than 0, to make sure the cluster driver node is placed on an on-demand instance. If this value is greater than or equal to the current cluster size, all @@ -664,126 +735,131 @@ github.com/databricks/databricks-sdk-go/service/compute.AzureAttributes: size, `first_on_demand` nodes will be placed on on-demand instances and the remainder will be placed on `availability` instances. Note that this value does not affect cluster size and cannot currently be mutated over the lifetime of a cluster. - log_analytics_info: - description: Defines values necessary to configure and run Azure Log Analytics - agent - spot_bid_max_price: - description: |- + "log_analytics_info": + "description": |- + Defines values necessary to configure and run Azure Log Analytics agent + "spot_bid_max_price": + "description": |- The max bid price to be used for Azure spot instances. The Max price for the bid cannot be higher than the on-demand price of the instance. If not specified, the default value is -1, which specifies that the instance cannot be evicted on the basis of price, and only on the basis of availability. Further, the value should > 0 or -1. github.com/databricks/databricks-sdk-go/service/compute.AzureAvailability: - _: - description: |- + "_": + "description": |- Availability type used for all subsequent nodes past the `first_on_demand` ones. Note: If `first_on_demand` is zero (which only happens on pool clusters), this availability type will be used for the entire cluster. - enum: - - SPOT_AZURE - - ON_DEMAND_AZURE - - SPOT_WITH_FALLBACK_AZURE + "enum": + - |- + SPOT_AZURE + - |- + ON_DEMAND_AZURE + - |- + SPOT_WITH_FALLBACK_AZURE github.com/databricks/databricks-sdk-go/service/compute.ClientsTypes: - jobs: - description: With jobs set, the cluster can be used for jobs - notebooks: - description: With notebooks set, this cluster can be used for notebooks + "jobs": + "description": |- + With jobs set, the cluster can be used for jobs + "notebooks": + "description": |- + With notebooks set, this cluster can be used for notebooks github.com/databricks/databricks-sdk-go/service/compute.ClusterLogConf: - dbfs: - description: |- + "dbfs": + "description": |- destination needs to be provided. e.g. `{ "dbfs" : { "destination" : "dbfs:/home/cluster_log" } }` - s3: - description: |- + "s3": + "description": |- destination and either the region or endpoint need to be provided. e.g. `{ "s3": { "destination" : "s3://cluster_log_bucket/prefix", "region" : "us-west-2" } }` Cluster iam role is used to access s3, please make sure the cluster iam role in `instance_profile_arn` has permission to write data to the s3 destination. github.com/databricks/databricks-sdk-go/service/compute.ClusterSpec: - 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. - autoscale: - description: |- + "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. + "autoscale": + "description": |- Parameters needed in order to automatically scale clusters up and down based on load. Note: autoscaling works best with DB runtime versions 3.0 or later. - autotermination_minutes: - description: |- + "autotermination_minutes": + "description": |- Automatically terminates the cluster after it is inactive for this time in minutes. If not set, this cluster will not be automatically terminated. If specified, the threshold must be between 10 and 10000 minutes. Users can also set this value to 0 to explicitly disable automatic termination. - aws_attributes: - description: |- + "aws_attributes": + "description": |- Attributes related to clusters running on Amazon Web Services. If not specified at cluster creation, a set of default values will be used. - azure_attributes: - description: |- + "azure_attributes": + "description": |- Attributes related to clusters running on Microsoft Azure. If not specified at cluster creation, a set of default values will be used. - cluster_log_conf: - description: |- + "cluster_log_conf": + "description": |- The configuration for delivering spark logs to a long-term storage destination. Two kinds of destinations (dbfs and s3) are supported. Only one destination can be specified for one cluster. If the conf is given, the logs will be delivered to the destination every `5 mins`. The destination of driver logs is `$destination/$clusterId/driver`, while the destination of executor logs is `$destination/$clusterId/executor`. - cluster_name: - description: | + "cluster_name": + "description": | Cluster name requested by the user. This doesn't have to be unique. If not specified at creation, the cluster name will be an empty string. - custom_tags: - description: |- + "custom_tags": + "description": |- Additional tags for cluster resources. Databricks will tag all cluster resources (e.g., AWS instances and EBS volumes) with these tags in addition to `default_tags`. Notes: - Currently, Databricks allows at most 45 custom tags - Clusters can only reuse cloud resources if the resources' tags are a subset of the cluster tags - data_security_mode: {} - docker_image: {} - driver_instance_pool_id: - description: |- + "data_security_mode": {} + "docker_image": {} + "driver_instance_pool_id": + "description": |- The optional ID of the instance pool for the driver of the cluster belongs. The pool cluster uses the instance pool with id (instance_pool_id) if the driver pool is not assigned. - driver_node_type_id: - description: | + "driver_node_type_id": + "description": | The node type of the Spark driver. Note that this field is optional; if unset, the driver node type will be set as the same value as `node_type_id` defined above. - enable_elastic_disk: - description: |- + "enable_elastic_disk": + "description": |- Autoscaling Local Storage: when enabled, this cluster will dynamically acquire additional disk space when its Spark workers are running low on disk space. This feature requires specific AWS permissions to function correctly - refer to the User Guide for more details. - enable_local_disk_encryption: - description: Whether to enable LUKS on cluster VMs' local disks - gcp_attributes: - description: |- + "enable_local_disk_encryption": + "description": |- + Whether to enable LUKS on cluster VMs' local disks + "gcp_attributes": + "description": |- Attributes related to clusters running on Google Cloud Platform. If not specified at cluster creation, a set of default values will be used. - 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 `//init_scripts`. - instance_pool_id: - description: The optional ID of the instance pool to which the cluster belongs. - is_single_node: - description: | + "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 `//init_scripts`. + "instance_pool_id": + "description": |- + The optional ID of the instance pool to which the cluster belongs. + "is_single_node": + "description": | This field can only be used with `kind`. When set to true, Databricks will automatically set single node related `custom_tags`, `spark_conf`, and `num_workers` - kind: {} - node_type_id: - description: | + "kind": {} + "node_type_id": + "description": | This field encodes, through a single value, the resources available to each of the Spark nodes in this cluster. For example, the Spark nodes can be provisioned and optimized for memory or compute intensive workloads. A list of available node types can be retrieved by using the :method:clusters/listNodeTypes API call. - num_workers: - description: |- + "num_workers": + "description": |- Number of worker nodes that this cluster should have. A cluster has one Spark Driver and `num_workers` Executors for a total of `num_workers` + 1 Spark nodes. @@ -792,18 +868,20 @@ github.com/databricks/databricks-sdk-go/service/compute.ClusterSpec: is resized from 5 to 10 workers, this field will immediately be updated to reflect the target size of 10 workers, whereas the workers listed in `spark_info` will gradually increase from 5 to 10 as the new nodes are provisioned. - policy_id: - description: The ID of the cluster policy used to create the cluster if applicable. - runtime_engine: {} - single_user_name: - description: Single user name if data_security_mode is `SINGLE_USER` - spark_conf: - description: | + "policy_id": + "description": |- + The ID of the cluster policy used to create the cluster if applicable. + "runtime_engine": {} + "single_user_name": + "description": |- + Single user name if data_security_mode is `SINGLE_USER` + "spark_conf": + "description": | An object containing a set of optional, user-specified Spark configuration key-value pairs. Users can also pass in a string of extra JVM options to the driver and the executors via `spark.driver.extraJavaOptions` and `spark.executor.extraJavaOptions` respectively. - spark_env_vars: - description: |- + "spark_env_vars": + "description": |- An object containing a set of optional, user-specified environment variable key-value pairs. Please note that key-value pair of the form (X,Y) will be exported as is (i.e., `export X='Y'`) while launching the driver and workers. @@ -815,25 +893,25 @@ github.com/databricks/databricks-sdk-go/service/compute.ClusterSpec: Example Spark environment variables: `{"SPARK_WORKER_MEMORY": "28000m", "SPARK_LOCAL_DIRS": "/local_disk0"}` or `{"SPARK_DAEMON_JAVA_OPTS": "$SPARK_DAEMON_JAVA_OPTS -Dspark.shuffle.service.enabled=true"}` - spark_version: - description: | + "spark_version": + "description": | The Spark version of the cluster, e.g. `3.3.x-scala2.11`. A list of available Spark versions can be retrieved by using the :method:clusters/sparkVersions API call. - ssh_public_keys: - description: |- + "ssh_public_keys": + "description": |- SSH public key contents that will be added to each Spark node in this cluster. The corresponding private keys can be used to login with the user name `ubuntu` on port `2200`. Up to 10 keys can be specified. - use_ml_runtime: - description: | + "use_ml_runtime": + "description": | This field can only be used with `kind`. `effective_spark_version` is determined by `spark_version` (DBR release), this field `use_ml_runtime`, and whether `node_type_id` is gpu node or not. - workload_type: {} + "workload_type": {} github.com/databricks/databricks-sdk-go/service/compute.DataSecurityMode: - _: - description: | + "_": + "description": | Data security mode decides what data governance model to use when accessing data from a cluster. @@ -854,193 +932,218 @@ github.com/databricks/databricks-sdk-go/service/compute.DataSecurityMode: * `LEGACY_PASSTHROUGH`: This mode is for users migrating from legacy Passthrough on high concurrency clusters. * `LEGACY_SINGLE_USER`: This mode is for users migrating from legacy Passthrough on standard clusters. * `LEGACY_SINGLE_USER_STANDARD`: This mode provides a way that doesn’t have UC nor passthrough enabled. - enum: - - DATA_SECURITY_MODE_AUTO - - DATA_SECURITY_MODE_STANDARD - - DATA_SECURITY_MODE_DEDICATED - - NONE - - SINGLE_USER - - USER_ISOLATION - - LEGACY_TABLE_ACL - - LEGACY_PASSTHROUGH - - LEGACY_SINGLE_USER - - LEGACY_SINGLE_USER_STANDARD + "enum": + - |- + DATA_SECURITY_MODE_AUTO + - |- + DATA_SECURITY_MODE_STANDARD + - |- + DATA_SECURITY_MODE_DEDICATED + - |- + NONE + - |- + SINGLE_USER + - |- + USER_ISOLATION + - |- + LEGACY_TABLE_ACL + - |- + LEGACY_PASSTHROUGH + - |- + LEGACY_SINGLE_USER + - |- + LEGACY_SINGLE_USER_STANDARD github.com/databricks/databricks-sdk-go/service/compute.DbfsStorageInfo: - destination: - description: dbfs destination, e.g. `dbfs:/my/path` + "destination": + "description": |- + dbfs destination, e.g. `dbfs:/my/path` github.com/databricks/databricks-sdk-go/service/compute.DockerBasicAuth: - password: - description: Password of the user - username: - description: Name of the user + "password": + "description": |- + Password of the user + "username": + "description": |- + Name of the user github.com/databricks/databricks-sdk-go/service/compute.DockerImage: - basic_auth: {} - url: - description: URL of the docker image. + "basic_auth": {} + "url": + "description": |- + URL of the docker image. github.com/databricks/databricks-sdk-go/service/compute.EbsVolumeType: - _: - description: The type of EBS volumes that will be launched with this cluster. - enum: - - GENERAL_PURPOSE_SSD - - THROUGHPUT_OPTIMIZED_HDD + "_": + "description": |- + The type of EBS volumes that will be launched with this cluster. + "enum": + - |- + GENERAL_PURPOSE_SSD + - |- + THROUGHPUT_OPTIMIZED_HDD github.com/databricks/databricks-sdk-go/service/compute.Environment: - _: - description: |- + "_": + "description": |- The environment entity used to preserve serverless environment side panel and jobs' environment for non-notebook task. In this minimal environment spec, only pip dependencies are supported. - client: - description: |- + "client": + "description": |- Client version used by the environment The client is the user-facing environment of the runtime. Each client comes with a specific set of pre-installed libraries. The version is a string, consisting of the major client version. - dependencies: - description: |- + "dependencies": + "description": |- List of pip dependencies, as supported by the version of pip in this environment. Each dependency is a pip requirement file line https://pip.pypa.io/en/stable/reference/requirements-file-format/ Allowed dependency could be , , (WSFS or Volumes in Databricks), E.g. dependencies: ["foo==0.0.1", "-r /Workspace/test/requirements.txt"] github.com/databricks/databricks-sdk-go/service/compute.GcpAttributes: - availability: {} - boot_disk_size: - description: boot disk size in GB - google_service_account: - description: |- + "availability": {} + "boot_disk_size": + "description": |- + boot disk size in GB + "google_service_account": + "description": |- If provided, the cluster will impersonate the google service account when accessing gcloud services (like GCS). The google service account must have previously been added to the Databricks environment by an account administrator. - local_ssd_count: - description: If provided, each node (workers and driver) in the cluster will have - this number of local SSDs attached. Each local SSD is 375GB in size. Refer to - [GCP documentation](https://cloud.google.com/compute/docs/disks/local-ssd#choose_number_local_ssds) - for the supported number of local SSDs for each instance type. - use_preemptible_executors: - description: |- + "local_ssd_count": + "description": |- + If provided, each node (workers and driver) in the cluster will have this number of local SSDs attached. Each local SSD is 375GB in size. Refer to [GCP documentation](https://cloud.google.com/compute/docs/disks/local-ssd#choose_number_local_ssds) for the supported number of local SSDs for each instance type. + "use_preemptible_executors": + "description": |- This field determines whether the spark executors will be scheduled to run on preemptible VMs (when set to true) versus standard compute engine VMs (when set to false; default). Note: Soon to be deprecated, use the availability field instead. - zone_id: - description: |- + "zone_id": + "description": |- Identifier for the availability zone in which the cluster resides. This can be one of the following: - "HA" => High availability, spread nodes across availability zones for a Databricks deployment region [default] - "AUTO" => Databricks picks an availability zone to schedule the cluster on. - A GCP availability zone => Pick One of the available zones for (machine type + region) from https://cloud.google.com/compute/docs/regions-zones. github.com/databricks/databricks-sdk-go/service/compute.GcpAvailability: - _: - description: |- + "_": + "description": |- This field determines whether the instance pool will contain preemptible VMs, on-demand VMs, or preemptible VMs with a fallback to on-demand VMs if the former is unavailable. - enum: - - PREEMPTIBLE_GCP - - ON_DEMAND_GCP - - PREEMPTIBLE_WITH_FALLBACK_GCP + "enum": + - |- + PREEMPTIBLE_GCP + - |- + ON_DEMAND_GCP + - |- + PREEMPTIBLE_WITH_FALLBACK_GCP github.com/databricks/databricks-sdk-go/service/compute.GcsStorageInfo: - destination: - description: GCS destination/URI, e.g. `gs://my-bucket/some-prefix` + "destination": + "description": |- + GCS destination/URI, e.g. `gs://my-bucket/some-prefix` github.com/databricks/databricks-sdk-go/service/compute.InitScriptInfo: - abfss: - description: |- + "abfss": + "description": |- destination needs to be provided. e.g. `{ "abfss" : { "destination" : "abfss://@.dfs.core.windows.net/" } } - dbfs: - description: |- + "dbfs": + "description": |- destination needs to be provided. e.g. `{ "dbfs" : { "destination" : "dbfs:/home/cluster_log" } }` - file: - description: |- + "file": + "description": |- destination needs to be provided. e.g. `{ "file" : { "destination" : "file:/my/local/file.sh" } }` - gcs: - description: |- + "gcs": + "description": |- destination needs to be provided. e.g. `{ "gcs": { "destination": "gs://my-bucket/file.sh" } }` - s3: - description: |- + "s3": + "description": |- destination and either the region or endpoint need to be provided. e.g. `{ "s3": { "destination" : "s3://cluster_log_bucket/prefix", "region" : "us-west-2" } }` Cluster iam role is used to access s3, please make sure the cluster iam role in `instance_profile_arn` has permission to write data to the s3 destination. - volumes: - description: |- + "volumes": + "description": |- destination needs to be provided. e.g. `{ "volumes" : { "destination" : "/Volumes/my-init.sh" } }` - workspace: - description: |- + "workspace": + "description": |- destination needs to be provided. e.g. `{ "workspace" : { "destination" : "/Users/user1@databricks.com/my-init.sh" } }` github.com/databricks/databricks-sdk-go/service/compute.Library: - cran: - description: Specification of a CRAN library to be installed as part of the library - egg: - description: Deprecated. URI of the egg library to install. Installing Python - egg files is deprecated and is not supported in Databricks Runtime 14.0 and - above. - jar: - description: |- + "cran": + "description": |- + Specification of a CRAN library to be installed as part of the library + "egg": + "description": |- + Deprecated. URI of the egg library to install. Installing Python egg files is deprecated and is not supported in Databricks Runtime 14.0 and above. + "jar": + "description": |- URI of the JAR library to install. Supported URIs include Workspace paths, Unity Catalog Volumes paths, and S3 URIs. For example: `{ "jar": "/Workspace/path/to/library.jar" }`, `{ "jar" : "/Volumes/path/to/library.jar" }` or `{ "jar": "s3://my-bucket/library.jar" }`. If S3 is used, please make sure the cluster has read access on the library. You may need to launch the cluster with an IAM role to access the S3 URI. - maven: - description: |- + "maven": + "description": |- Specification of a maven library to be installed. For example: `{ "coordinates": "org.jsoup:jsoup:1.7.2" }` - pypi: - description: |- + "pypi": + "description": |- Specification of a PyPi library to be installed. For example: `{ "package": "simplejson" }` - requirements: - description: |- + "requirements": + "description": |- URI of the requirements.txt file to install. Only Workspace paths and Unity Catalog Volumes paths are supported. For example: `{ "requirements": "/Workspace/path/to/requirements.txt" }` or `{ "requirements" : "/Volumes/path/to/requirements.txt" }` - whl: - description: |- + "whl": + "description": |- URI of the wheel library to install. Supported URIs include Workspace paths, Unity Catalog Volumes paths, and S3 URIs. For example: `{ "whl": "/Workspace/path/to/library.whl" }`, `{ "whl" : "/Volumes/path/to/library.whl" }` or `{ "whl": "s3://my-bucket/library.whl" }`. If S3 is used, please make sure the cluster has read access on the library. You may need to launch the cluster with an IAM role to access the S3 URI. github.com/databricks/databricks-sdk-go/service/compute.LocalFileInfo: - destination: - description: local file destination, e.g. `file:/my/local/file.sh` + "destination": + "description": |- + local file destination, e.g. `file:/my/local/file.sh` github.com/databricks/databricks-sdk-go/service/compute.LogAnalyticsInfo: - log_analytics_primary_key: - description: - log_analytics_workspace_id: - description: + "log_analytics_primary_key": + "description": |- + + "log_analytics_workspace_id": + "description": |- + github.com/databricks/databricks-sdk-go/service/compute.MavenLibrary: - coordinates: - description: 'Gradle-style maven coordinates. For example: "org.jsoup:jsoup:1.7.2".' - exclusions: - description: |- + "coordinates": + "description": |- + Gradle-style maven coordinates. For example: "org.jsoup:jsoup:1.7.2". + "exclusions": + "description": |- List of dependences to exclude. For example: `["slf4j:slf4j", "*:hadoop-client"]`. Maven dependency exclusions: https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html. - repo: - description: |- + "repo": + "description": |- Maven repo to install the Maven package from. If omitted, both Maven Central Repository and Spark Packages are searched. github.com/databricks/databricks-sdk-go/service/compute.PythonPyPiLibrary: - package: - description: |- + "package": + "description": |- The name of the pypi package to install. An optional exact version specification is also supported. Examples: "simplejson" and "simplejson==3.8.0". - repo: - description: |- + "repo": + "description": |- The repository where the package can be found. If not specified, the default pip index is used. github.com/databricks/databricks-sdk-go/service/compute.RCranLibrary: - package: - description: The name of the CRAN package to install. - repo: - description: The repository where the package can be found. If not specified, - the default CRAN repo is used. + "package": + "description": |- + The name of the CRAN package to install. + "repo": + "description": |- + The repository where the package can be found. If not specified, the default CRAN repo is used. github.com/databricks/databricks-sdk-go/service/compute.RuntimeEngine: - _: - description: | + "_": + "description": | Determines the cluster's runtime engine, either standard or Photon. This field is not compatible with legacy `spark_version` values that contain `-photon-`. @@ -1048,13 +1151,16 @@ github.com/databricks/databricks-sdk-go/service/compute.RuntimeEngine: If left unspecified, the runtime engine defaults to standard unless the spark_version contains -photon-, in which case Photon will be used. - enum: - - "NULL" - - STANDARD - - PHOTON + "enum": + - |- + NULL + - |- + STANDARD + - |- + PHOTON github.com/databricks/databricks-sdk-go/service/compute.S3StorageInfo: - canned_acl: - description: |- + "canned_acl": + "description": |- (Optional) Set canned access control list for the logs, e.g. `bucket-owner-full-control`. If `canned_cal` is set, please make sure the cluster iam role has `s3:PutObjectAcl` permission on the destination bucket and prefix. The full list of possible canned acl can be found at @@ -1062,329 +1168,352 @@ github.com/databricks/databricks-sdk-go/service/compute.S3StorageInfo: Please also note that by default only the object owner gets full controls. If you are using cross account role for writing data, you may want to set `bucket-owner-full-control` to make bucket owner able to read the logs. - destination: - description: |- + "destination": + "description": |- S3 destination, e.g. `s3://my-bucket/some-prefix` Note that logs will be delivered using cluster iam role, please make sure you set cluster iam role and the role has write access to the destination. Please also note that you cannot use AWS keys to deliver logs. - enable_encryption: - description: (Optional) Flag to enable server side encryption, `false` by default. - encryption_type: - description: |- + "enable_encryption": + "description": |- + (Optional) Flag to enable server side encryption, `false` by default. + "encryption_type": + "description": |- (Optional) The encryption type, it could be `sse-s3` or `sse-kms`. It will be used only when encryption is enabled and the default type is `sse-s3`. - endpoint: - description: |- + "endpoint": + "description": |- S3 endpoint, e.g. `https://s3-us-west-2.amazonaws.com`. Either region or endpoint needs to be set. If both are set, endpoint will be used. - kms_key: - description: (Optional) Kms key which will be used if encryption is enabled and - encryption type is set to `sse-kms`. - region: - description: |- + "kms_key": + "description": |- + (Optional) Kms key which will be used if encryption is enabled and encryption type is set to `sse-kms`. + "region": + "description": |- S3 region, e.g. `us-west-2`. Either region or endpoint needs to be set. If both are set, endpoint will be used. github.com/databricks/databricks-sdk-go/service/compute.VolumesStorageInfo: - destination: - description: Unity Catalog Volumes file destination, e.g. `/Volumes/my-init.sh` + "destination": + "description": |- + Unity Catalog Volumes file destination, e.g. `/Volumes/my-init.sh` github.com/databricks/databricks-sdk-go/service/compute.WorkloadType: - clients: - description: ' defined what type of clients can use the cluster. E.g. Notebooks, - Jobs' + "clients": + "description": |2- + defined what type of clients can use the cluster. E.g. Notebooks, Jobs github.com/databricks/databricks-sdk-go/service/compute.WorkspaceStorageInfo: - destination: - description: workspace files destination, e.g. `/Users/user1@databricks.com/my-init.sh` + "destination": + "description": |- + workspace files destination, e.g. `/Users/user1@databricks.com/my-init.sh` github.com/databricks/databricks-sdk-go/service/dashboards.LifecycleState: - _: - enum: - - ACTIVE - - TRASHED + "_": + "enum": + - |- + ACTIVE + - |- + TRASHED github.com/databricks/databricks-sdk-go/service/jobs.CleanRoomsNotebookTask: - clean_room_name: - description: The clean room that the notebook belongs to. - etag: - description: |- + "clean_room_name": + "description": |- + The clean room that the notebook belongs to. + "etag": + "description": |- Checksum to validate the freshness of the notebook resource (i.e. the notebook being run is the latest version). It can be fetched by calling the :method:cleanroomassets/get API. - notebook_base_parameters: - description: Base parameters to be used for the clean room notebook job. - notebook_name: - description: Name of the notebook being run. + "notebook_base_parameters": + "description": |- + Base parameters to be used for the clean room notebook job. + "notebook_name": + "description": |- + Name of the notebook being run. github.com/databricks/databricks-sdk-go/service/jobs.Condition: - _: - enum: - - ANY_UPDATED - - ALL_UPDATED + "_": + "enum": + - |- + ANY_UPDATED + - |- + ALL_UPDATED github.com/databricks/databricks-sdk-go/service/jobs.ConditionTask: - left: - description: The left operand of the condition task. Can be either a string value - or a job state or parameter reference. - op: - description: |- + "left": + "description": |- + The left operand of the condition task. Can be either a string value or a job state or parameter reference. + "op": + "description": |- * `EQUAL_TO`, `NOT_EQUAL` operators perform string comparison of their operands. This means that `“12.0” == “12”` will evaluate to `false`. * `GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN`, `LESS_THAN_OR_EQUAL` operators perform numeric comparison of their operands. `“12.0” >= “12”` will evaluate to `true`, `“10.0” >= “12”` will evaluate to `false`. The boolean comparison to task values can be implemented with operators `EQUAL_TO`, `NOT_EQUAL`. If a task value was set to a boolean value, it will be serialized to `“true”` or `“false”` for the comparison. - right: - description: The right operand of the condition task. Can be either a string value - or a job state or parameter reference. + "right": + "description": |- + The right operand of the condition task. Can be either a string value or a job state or parameter reference. github.com/databricks/databricks-sdk-go/service/jobs.ConditionTaskOp: - _: - description: |- + "_": + "description": |- * `EQUAL_TO`, `NOT_EQUAL` operators perform string comparison of their operands. This means that `“12.0” == “12”` will evaluate to `false`. * `GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN`, `LESS_THAN_OR_EQUAL` operators perform numeric comparison of their operands. `“12.0” >= “12”` will evaluate to `true`, `“10.0” >= “12”` will evaluate to `false`. The boolean comparison to task values can be implemented with operators `EQUAL_TO`, `NOT_EQUAL`. If a task value was set to a boolean value, it will be serialized to `“true”` or `“false”` for the comparison. - enum: - - EQUAL_TO - - GREATER_THAN - - GREATER_THAN_OR_EQUAL - - LESS_THAN - - LESS_THAN_OR_EQUAL - - NOT_EQUAL + "enum": + - |- + EQUAL_TO + - |- + GREATER_THAN + - |- + GREATER_THAN_OR_EQUAL + - |- + LESS_THAN + - |- + LESS_THAN_OR_EQUAL + - |- + NOT_EQUAL github.com/databricks/databricks-sdk-go/service/jobs.Continuous: - pause_status: - description: Indicate whether the continuous execution of the job is paused or - not. Defaults to UNPAUSED. + "pause_status": + "description": |- + Indicate whether the continuous execution of the job is paused or not. Defaults to UNPAUSED. github.com/databricks/databricks-sdk-go/service/jobs.CronSchedule: - pause_status: - description: Indicate whether this schedule is paused or not. - quartz_cron_expression: - description: A Cron expression using Quartz syntax that describes the schedule - for a job. See [Cron Trigger](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) - for details. This field is required. - timezone_id: - description: A Java timezone ID. The schedule for a job is resolved with respect - to this timezone. See [Java TimeZone](https://docs.oracle.com/javase/7/docs/api/java/util/TimeZone.html) - for details. This field is required. + "pause_status": + "description": |- + Indicate whether this schedule is paused or not. + "quartz_cron_expression": + "description": |- + A Cron expression using Quartz syntax that describes the schedule for a job. See [Cron Trigger](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) for details. This field is required. + "timezone_id": + "description": |- + A Java timezone ID. The schedule for a job is resolved with respect to this timezone. See [Java TimeZone](https://docs.oracle.com/javase/7/docs/api/java/util/TimeZone.html) for details. This field is required. github.com/databricks/databricks-sdk-go/service/jobs.DbtTask: - catalog: - description: Optional name of the catalog to use. The value is the top level in - the 3-level namespace of Unity Catalog (catalog / schema / relation). The catalog - value can only be specified if a warehouse_id is specified. Requires dbt-databricks - >= 1.1.1. - commands: - description: A list of dbt commands to execute. All commands must start with `dbt`. - This parameter must not be empty. A maximum of up to 10 commands can be provided. - profiles_directory: - description: Optional (relative) path to the profiles directory. Can only be specified - if no warehouse_id is specified. If no warehouse_id is specified and this folder - is unset, the root directory is used. - project_directory: - description: |- + "catalog": + "description": |- + Optional name of the catalog to use. The value is the top level in the 3-level namespace of Unity Catalog (catalog / schema / relation). The catalog value can only be specified if a warehouse_id is specified. Requires dbt-databricks >= 1.1.1. + "commands": + "description": |- + A list of dbt commands to execute. All commands must start with `dbt`. This parameter must not be empty. A maximum of up to 10 commands can be provided. + "profiles_directory": + "description": |- + Optional (relative) path to the profiles directory. Can only be specified if no warehouse_id is specified. If no warehouse_id is specified and this folder is unset, the root directory is used. + "project_directory": + "description": |- Path to the project directory. Optional for Git sourced tasks, in which case if no value is provided, the root of the Git repository is used. - schema: - description: Optional schema to write to. This parameter is only used when a warehouse_id - is also provided. If not provided, the `default` schema is used. - source: - description: |- + "schema": + "description": |- + Optional schema to write to. This parameter is only used when a warehouse_id is also provided. If not provided, the `default` schema is used. + "source": + "description": |- Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved from the local Databricks workspace. When set to `GIT`, the project will be retrieved from a Git repository defined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise. * `WORKSPACE`: Project is located in Databricks workspace. * `GIT`: Project is located in cloud Git provider. - warehouse_id: - description: ID of the SQL warehouse to connect to. If provided, we automatically - generate and provide the profile and connection details to dbt. It can be overridden - on a per-command basis by using the `--profiles-dir` command line argument. + "warehouse_id": + "description": |- + ID of the SQL warehouse to connect to. If provided, we automatically generate and provide the profile and connection details to dbt. It can be overridden on a per-command basis by using the `--profiles-dir` command line argument. github.com/databricks/databricks-sdk-go/service/jobs.FileArrivalTriggerConfiguration: - min_time_between_triggers_seconds: - description: |- + "min_time_between_triggers_seconds": + "description": |- If set, the trigger starts a run only after the specified amount of time passed since the last time the trigger fired. The minimum allowed value is 60 seconds - url: - description: URL to be monitored for file arrivals. The path must point to the - root or a subpath of the external location. - wait_after_last_change_seconds: - description: |- + "url": + "description": |- + URL to be monitored for file arrivals. The path must point to the root or a subpath of the external location. + "wait_after_last_change_seconds": + "description": |- If set, the trigger starts a run only after no file activity has occurred for the specified amount of time. This makes it possible to wait for a batch of incoming files to arrive before triggering a run. The minimum allowed value is 60 seconds. github.com/databricks/databricks-sdk-go/service/jobs.ForEachTask: - concurrency: - description: |- + "concurrency": + "description": |- An optional maximum allowed number of concurrent runs of the task. Set this value if you want to be able to execute multiple runs of the task concurrently. - inputs: - description: |- + "inputs": + "description": |- Array for task to iterate on. This can be a JSON string or a reference to an array parameter. - task: - description: Configuration for the task that will be run for each element in the - array + "task": + "description": |- + Configuration for the task that will be run for each element in the array github.com/databricks/databricks-sdk-go/service/jobs.Format: - _: - enum: - - SINGLE_TASK - - MULTI_TASK + "_": + "enum": + - |- + SINGLE_TASK + - |- + MULTI_TASK github.com/databricks/databricks-sdk-go/service/jobs.GitProvider: - _: - enum: - - gitHub - - bitbucketCloud - - azureDevOpsServices - - gitHubEnterprise - - bitbucketServer - - gitLab - - gitLabEnterpriseEdition - - awsCodeCommit + "_": + "enum": + - |- + gitHub + - |- + bitbucketCloud + - |- + azureDevOpsServices + - |- + gitHubEnterprise + - |- + bitbucketServer + - |- + gitLab + - |- + gitLabEnterpriseEdition + - |- + awsCodeCommit github.com/databricks/databricks-sdk-go/service/jobs.GitSnapshot: - _: - description: Read-only state of the remote repository at the time the job was - run. This field is only included on job runs. - used_commit: - description: Commit that was used to execute the run. If git_branch was specified, - this points to the HEAD of the branch at the time of the run; if git_tag was - specified, this points to the commit the tag points to. + "_": + "description": |- + Read-only state of the remote repository at the time the job was run. This field is only included on job runs. + "used_commit": + "description": |- + Commit that was used to execute the run. If git_branch was specified, this points to the HEAD of the branch at the time of the run; if git_tag was specified, this points to the commit the tag points to. github.com/databricks/databricks-sdk-go/service/jobs.GitSource: - _: - description: |- + "_": + "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. If `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. Note: 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. - git_branch: - description: Name of the branch to be checked out and used by this job. This field - cannot be specified in conjunction with git_tag or git_commit. - git_commit: - description: Commit to be checked out and used by this job. This field cannot - be specified in conjunction with git_branch or git_tag. - git_provider: - description: Unique identifier of the service used to host the Git repository. - The value is case insensitive. - git_snapshot: {} - 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. - git_url: - description: URL of the repository to be cloned by this job. - job_source: - description: The source of the job specification in the remote repository when - the job is source controlled. + "git_branch": + "description": |- + Name of the branch to be checked out and used by this job. This field cannot be specified in conjunction with git_tag or git_commit. + "git_commit": + "description": |- + Commit to be checked out and used by this job. This field cannot be specified in conjunction with git_branch or git_tag. + "git_provider": + "description": |- + Unique identifier of the service used to host the Git repository. The value is case insensitive. + "git_snapshot": {} + "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. + "git_url": + "description": |- + URL of the repository to be cloned by this job. + "job_source": + "description": |- + The source of the job specification in the remote repository when the job is source controlled. github.com/databricks/databricks-sdk-go/service/jobs.JobCluster: - job_cluster_key: - description: |- + "job_cluster_key": + "description": |- A unique name for the job cluster. This field is required and must be unique within the job. `JobTaskSettings` may refer to this field to determine which cluster to launch for the task execution. - new_cluster: - description: If new_cluster, a description of a cluster that is created for each - task. + "new_cluster": + "description": |- + If new_cluster, a description of a cluster that is created for each task. github.com/databricks/databricks-sdk-go/service/jobs.JobDeployment: - kind: - description: |- + "kind": + "description": |- The kind of deployment that manages the job. * `BUNDLE`: The job is managed by Databricks Asset Bundle. - metadata_file_path: - description: Path of the file that contains deployment metadata. + "metadata_file_path": + "description": |- + Path of the file that contains deployment metadata. github.com/databricks/databricks-sdk-go/service/jobs.JobDeploymentKind: - _: - description: '* `BUNDLE`: The job is managed by Databricks Asset Bundle.' - enum: - - BUNDLE + "_": + "description": |- + * `BUNDLE`: The job is managed by Databricks Asset Bundle. + "enum": + - |- + BUNDLE github.com/databricks/databricks-sdk-go/service/jobs.JobEditMode: - _: - description: |- + "_": + "description": |- Edit mode of the job. * `UI_LOCKED`: The job is in a locked UI state and cannot be modified. * `EDITABLE`: The job is in an editable state and can be modified. - enum: - - UI_LOCKED - - EDITABLE + "enum": + - |- + UI_LOCKED + - |- + EDITABLE github.com/databricks/databricks-sdk-go/service/jobs.JobEmailNotifications: - no_alert_for_skipped_runs: - description: |- + "no_alert_for_skipped_runs": + "description": |- If true, do not send email to recipients specified in `on_failure` if the run is skipped. This field is `deprecated`. Please use the `notification_settings.no_alert_for_skipped_runs` field. - on_duration_warning_threshold_exceeded: - description: A list of email addresses to be notified when the duration of a run - exceeds the threshold specified for the `RUN_DURATION_SECONDS` metric in the - `health` field. If no rule for the `RUN_DURATION_SECONDS` metric is specified - in the `health` field for the job, notifications are not sent. - on_failure: - description: A list of email addresses to be notified when a run unsuccessfully - completes. A run is considered to have completed unsuccessfully if it ends with - an `INTERNAL_ERROR` `life_cycle_state` or a `FAILED`, or `TIMED_OUT` result_state. - If this is not specified on job creation, reset, or update the list is empty, - and notifications are not sent. - on_start: - description: A list of email addresses to be notified when a run begins. If not - specified on job creation, reset, or update, the list is empty, and notifications - are not sent. - on_streaming_backlog_exceeded: - description: |- + "on_duration_warning_threshold_exceeded": + "description": |- + A list of email addresses to be notified when the duration of a run exceeds the threshold specified for the `RUN_DURATION_SECONDS` metric in the `health` field. If no rule for the `RUN_DURATION_SECONDS` metric is specified in the `health` field for the job, notifications are not sent. + "on_failure": + "description": |- + A list of email addresses to be notified when a run unsuccessfully completes. A run is considered to have completed unsuccessfully if it ends with an `INTERNAL_ERROR` `life_cycle_state` or a `FAILED`, or `TIMED_OUT` result_state. If this is not specified on job creation, reset, or update the list is empty, and notifications are not sent. + "on_start": + "description": |- + A list of email addresses to be notified when a run begins. If not specified on job creation, reset, or update, the list is empty, and notifications are not sent. + "on_streaming_backlog_exceeded": + "description": |- A list of email addresses to notify when any streaming backlog thresholds are exceeded for any stream. Streaming backlog thresholds can be set in the `health` field using the following metrics: `STREAMING_BACKLOG_BYTES`, `STREAMING_BACKLOG_RECORDS`, `STREAMING_BACKLOG_SECONDS`, or `STREAMING_BACKLOG_FILES`. Alerting is based on the 10-minute average of these metrics. If the issue persists, notifications are resent every 30 minutes. - on_success: - description: A list of email addresses to be notified when a run successfully - completes. A run is considered to have completed successfully if it ends with - a `TERMINATED` `life_cycle_state` and a `SUCCESS` result_state. If not specified - on job creation, reset, or update, the list is empty, and notifications are - not sent. + "on_success": + "description": |- + A list of email addresses to be notified when a run successfully completes. A run is considered to have completed successfully if it ends with a `TERMINATED` `life_cycle_state` and a `SUCCESS` result_state. If not specified on job creation, reset, or update, the list is empty, and notifications are not sent. github.com/databricks/databricks-sdk-go/service/jobs.JobEnvironment: - environment_key: - description: The key of an environment. It has to be unique within a job. - spec: {} + "environment_key": + "description": |- + The key of an environment. It has to be unique within a job. + "spec": {} github.com/databricks/databricks-sdk-go/service/jobs.JobNotificationSettings: - no_alert_for_canceled_runs: - description: If true, do not send notifications to recipients specified in `on_failure` - if the run is canceled. - no_alert_for_skipped_runs: - description: If true, do not send notifications to recipients specified in `on_failure` - if the run is skipped. + "no_alert_for_canceled_runs": + "description": |- + If true, do not send notifications to recipients specified in `on_failure` if the run is canceled. + "no_alert_for_skipped_runs": + "description": |- + If true, do not send notifications to recipients specified in `on_failure` if the run is skipped. github.com/databricks/databricks-sdk-go/service/jobs.JobParameterDefinition: - default: - description: Default value of the parameter. - name: - description: The name of the defined parameter. May only contain alphanumeric - characters, `_`, `-`, and `.` + "default": + "description": |- + Default value of the parameter. + "name": + "description": |- + The name of the defined parameter. May only contain alphanumeric characters, `_`, `-`, and `.` github.com/databricks/databricks-sdk-go/service/jobs.JobRunAs: - _: - description: |- + "_": + "description": |- Write-only setting. Specifies the user or service principal that the job runs as. If not specified, the job runs as the user who created the job. Either `user_name` or `service_principal_name` should be specified. If not, an error is thrown. - service_principal_name: - description: Application ID of an active service principal. Setting this field - requires the `servicePrincipal/user` role. - user_name: - description: The email of an active workspace user. Non-admin users can only set - this field to their own email. + "service_principal_name": + "description": |- + Application ID of an active service principal. Setting this field requires the `servicePrincipal/user` role. + "user_name": + "description": |- + The email of an active workspace user. Non-admin users can only set this field to their own email. github.com/databricks/databricks-sdk-go/service/jobs.JobSource: - _: - description: The source of the job specification in the remote repository when - the job is source controlled. - dirty_state: - description: |- + "_": + "description": |- + The source of the job specification in the remote repository when the job is source controlled. + "dirty_state": + "description": |- Dirty state indicates the job is not fully synced with the job specification in the remote repository. Possible values are: * `NOT_SYNCED`: The job is not yet synced with the remote job specification. Import the remote job specification from UI to make the job fully synced. * `DISCONNECTED`: The job is temporary disconnected from the remote job specification and is allowed for live edit. Import the remote job specification again from UI to make the job fully synced. - import_from_git_branch: - description: Name of the branch which the job is imported from. - job_config_path: - description: Path of the job YAML file that contains the job specification. + "import_from_git_branch": + "description": |- + Name of the branch which the job is imported from. + "job_config_path": + "description": |- + Path of the job YAML file that contains the job specification. github.com/databricks/databricks-sdk-go/service/jobs.JobSourceDirtyState: - _: - description: |- + "_": + "description": |- Dirty state indicates the job is not fully synced with the job specification in the remote repository. Possible values are: * `NOT_SYNCED`: The job is not yet synced with the remote job specification. Import the remote job specification from UI to make the job fully synced. * `DISCONNECTED`: The job is temporary disconnected from the remote job specification and is allowed for live edit. Import the remote job specification again from UI to make the job fully synced. - enum: - - NOT_SYNCED - - DISCONNECTED + "enum": + - |- + NOT_SYNCED + - |- + DISCONNECTED github.com/databricks/databricks-sdk-go/service/jobs.JobsHealthMetric: - _: - description: |- + "_": + "description": |- Specifies the health metric that is being evaluated for a particular health rule. * `RUN_DURATION_SECONDS`: Expected total time for a run in seconds. @@ -1392,31 +1521,38 @@ github.com/databricks/databricks-sdk-go/service/jobs.JobsHealthMetric: * `STREAMING_BACKLOG_RECORDS`: An estimate of the maximum offset lag across all streams. This metric is in Public Preview. * `STREAMING_BACKLOG_SECONDS`: An estimate of the maximum consumer delay across all streams. This metric is in Public Preview. * `STREAMING_BACKLOG_FILES`: An estimate of the maximum number of outstanding files across all streams. This metric is in Public Preview. - enum: - - RUN_DURATION_SECONDS - - STREAMING_BACKLOG_BYTES - - STREAMING_BACKLOG_RECORDS - - STREAMING_BACKLOG_SECONDS - - STREAMING_BACKLOG_FILES + "enum": + - |- + RUN_DURATION_SECONDS + - |- + STREAMING_BACKLOG_BYTES + - |- + STREAMING_BACKLOG_RECORDS + - |- + STREAMING_BACKLOG_SECONDS + - |- + STREAMING_BACKLOG_FILES github.com/databricks/databricks-sdk-go/service/jobs.JobsHealthOperator: - _: - description: Specifies the operator used to compare the health metric value with - the specified threshold. - enum: - - GREATER_THAN + "_": + "description": |- + Specifies the operator used to compare the health metric value with the specified threshold. + "enum": + - |- + GREATER_THAN github.com/databricks/databricks-sdk-go/service/jobs.JobsHealthRule: - metric: {} - op: {} - value: - description: Specifies the threshold value that the health metric should obey - to satisfy the health rule. + "metric": {} + "op": {} + "value": + "description": |- + Specifies the threshold value that the health metric should obey to satisfy the health rule. github.com/databricks/databricks-sdk-go/service/jobs.JobsHealthRules: - _: - description: An optional set of health rules that can be defined for this job. - rules: {} + "_": + "description": |- + An optional set of health rules that can be defined for this job. + "rules": {} github.com/databricks/databricks-sdk-go/service/jobs.NotebookTask: - base_parameters: - description: |- + "base_parameters": + "description": |- Base parameters to be used for each run of this job. If the run is initiated by a call to :method:jobs/run Now with parameters specified, the two parameters maps are merged. If the same key is specified in `base_parameters` and in `run-now`, the value from `run-now` is used. @@ -1428,65 +1564,76 @@ github.com/databricks/databricks-sdk-go/service/jobs.NotebookTask: Retrieve these parameters in a notebook using [dbutils.widgets.get](https://docs.databricks.com/dev-tools/databricks-utils.html#dbutils-widgets). The JSON representation of this field cannot exceed 1MB. - notebook_path: - description: |- + "notebook_path": + "description": |- The path of the notebook to be run in the Databricks workspace or remote repository. For notebooks stored in the Databricks workspace, the path must be absolute and begin with a slash. For notebooks stored in a remote repository, the path must be relative. This field is required. - source: - description: |- + "source": + "description": |- Optional location type of the notebook. When set to `WORKSPACE`, the notebook will be retrieved from the local Databricks workspace. When set to `GIT`, the notebook will be retrieved from a Git repository defined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise. * `WORKSPACE`: Notebook is located in Databricks workspace. * `GIT`: Notebook is located in cloud Git provider. - warehouse_id: - description: |- + "warehouse_id": + "description": |- Optional `warehouse_id` to run the notebook on a SQL warehouse. Classic SQL warehouses are NOT supported, please use serverless or pro SQL warehouses. Note that SQL warehouses only support SQL cells; if the notebook contains non-SQL cells, the run will fail. github.com/databricks/databricks-sdk-go/service/jobs.PauseStatus: - _: - enum: - - UNPAUSED - - PAUSED + "_": + "enum": + - |- + UNPAUSED + - |- + PAUSED github.com/databricks/databricks-sdk-go/service/jobs.PeriodicTriggerConfiguration: - interval: - description: The interval at which the trigger should run. - unit: - description: The unit of time for the interval. + "interval": + "description": |- + The interval at which the trigger should run. + "unit": + "description": |- + The unit of time for the interval. github.com/databricks/databricks-sdk-go/service/jobs.PeriodicTriggerConfigurationTimeUnit: - _: - enum: - - HOURS - - DAYS - - WEEKS + "_": + "enum": + - |- + HOURS + - |- + DAYS + - |- + WEEKS github.com/databricks/databricks-sdk-go/service/jobs.PipelineParams: - full_refresh: - description: If true, triggers a full refresh on the delta live table. + "full_refresh": + "description": |- + If true, triggers a full refresh on the delta live table. github.com/databricks/databricks-sdk-go/service/jobs.PipelineTask: - full_refresh: - description: If true, triggers a full refresh on the delta live table. - pipeline_id: - description: The full name of the pipeline task to execute. + "full_refresh": + "description": |- + If true, triggers a full refresh on the delta live table. + "pipeline_id": + "description": |- + The full name of the pipeline task to execute. github.com/databricks/databricks-sdk-go/service/jobs.PythonWheelTask: - entry_point: - description: Named entry point to use, if it does not exist in the metadata of - the package it executes the function from the package directly using `$packageName.$entryPoint()` - named_parameters: - description: Command-line parameters passed to Python wheel task in the form of - `["--name=task", "--data=dbfs:/path/to/data.json"]`. Leave it empty if `parameters` - is not null. - package_name: - description: Name of the package to execute - parameters: - description: Command-line parameters passed to Python wheel task. Leave it empty - if `named_parameters` is not null. + "entry_point": + "description": |- + Named entry point to use, if it does not exist in the metadata of the package it executes the function from the package directly using `$packageName.$entryPoint()` + "named_parameters": + "description": |- + Command-line parameters passed to Python wheel task in the form of `["--name=task", "--data=dbfs:/path/to/data.json"]`. Leave it empty if `parameters` is not null. + "package_name": + "description": |- + Name of the package to execute + "parameters": + "description": |- + Command-line parameters passed to Python wheel task. Leave it empty if `named_parameters` is not null. github.com/databricks/databricks-sdk-go/service/jobs.QueueSettings: - enabled: - description: If true, enable queueing for the job. This is a required field. + "enabled": + "description": |- + If true, enable queueing for the job. This is a required field. github.com/databricks/databricks-sdk-go/service/jobs.RunIf: - _: - description: |- + "_": + "description": |- An optional value indicating the condition that determines whether the task should be run once its dependencies have been completed. When omitted, defaults to `ALL_SUCCESS`. Possible values are: @@ -1496,20 +1643,25 @@ github.com/databricks/databricks-sdk-go/service/jobs.RunIf: * `ALL_DONE`: All dependencies have been completed * `AT_LEAST_ONE_FAILED`: At least one dependency failed * `ALL_FAILED`: ALl dependencies have failed - enum: - - ALL_SUCCESS - - ALL_DONE - - NONE_FAILED - - AT_LEAST_ONE_SUCCESS - - ALL_FAILED - - AT_LEAST_ONE_FAILED + "enum": + - |- + ALL_SUCCESS + - |- + ALL_DONE + - |- + NONE_FAILED + - |- + AT_LEAST_ONE_SUCCESS + - |- + ALL_FAILED + - |- + AT_LEAST_ONE_FAILED github.com/databricks/databricks-sdk-go/service/jobs.RunJobTask: - dbt_commands: - description: 'An array of commands to execute for jobs with the dbt task, for - example `"dbt_commands": ["dbt deps", "dbt seed", "dbt deps", "dbt seed", "dbt - run"]`' - jar_params: - description: |- + "dbt_commands": + "description": |- + An array of commands to execute for jobs with the dbt task, for example `"dbt_commands": ["dbt deps", "dbt seed", "dbt deps", "dbt seed", "dbt run"]` + "jar_params": + "description": |- A list of parameters for jobs with Spark JAR tasks, for example `"jar_params": ["john doe", "35"]`. The parameters are used to invoke the main function of the main class specified in the Spark JAR task. If not specified upon `run-now`, it defaults to an empty list. @@ -1517,12 +1669,14 @@ github.com/databricks/databricks-sdk-go/service/jobs.RunJobTask: The JSON representation of this field (for example `{"jar_params":["john doe","35"]}`) cannot exceed 10,000 bytes. Use [Task parameter variables](https://docs.databricks.com/jobs.html#parameter-variables) to set parameters containing information about job runs. - job_id: - description: ID of the job to trigger. - job_parameters: - description: Job-level parameters used to trigger the job. - notebook_params: - description: |- + "job_id": + "description": |- + ID of the job to trigger. + "job_parameters": + "description": |- + Job-level parameters used to trigger the job. + "notebook_params": + "description": |- A map from keys to values for jobs with notebook task, for example `"notebook_params": {"name": "john doe", "age": "35"}`. The map is passed to the notebook and is accessible through the [dbutils.widgets.get](https://docs.databricks.com/dev-tools/databricks-utils.html) function. @@ -1533,11 +1687,12 @@ github.com/databricks/databricks-sdk-go/service/jobs.RunJobTask: Use [Task parameter variables](https://docs.databricks.com/jobs.html#parameter-variables) to set parameters containing information about job runs. The JSON representation of this field (for example `{"notebook_params":{"name":"john doe","age":"35"}}`) cannot exceed 10,000 bytes. - pipeline_params: - description: Controls whether the pipeline should perform a full refresh - python_named_params: {} - python_params: - description: |- + "pipeline_params": + "description": |- + Controls whether the pipeline should perform a full refresh + "python_named_params": {} + "python_params": + "description": |- A list of parameters for jobs with Python tasks, for example `"python_params": ["john doe", "35"]`. The parameters are passed to Python file as command-line parameters. If specified upon `run-now`, it would overwrite the parameters specified in job setting. The JSON representation of this field (for example `{"python_params":["john doe","35"]}`) @@ -1549,8 +1704,8 @@ github.com/databricks/databricks-sdk-go/service/jobs.RunJobTask: These parameters accept only Latin characters (ASCII character set). Using non-ASCII characters returns an error. Examples of invalid, non-ASCII characters are Chinese, Japanese kanjis, and emojis. - spark_submit_params: - description: |- + "spark_submit_params": + "description": |- A list of parameters for jobs with spark submit task, for example `"spark_submit_params": ["--class", "org.apache.spark.examples.SparkPi"]`. The parameters are passed to spark-submit script as command-line parameters. If specified upon `run-now`, it would overwrite the parameters specified in job setting. The JSON representation of this field (for example `{"python_params":["john doe","35"]}`) @@ -1562,50 +1717,48 @@ github.com/databricks/databricks-sdk-go/service/jobs.RunJobTask: These parameters accept only Latin characters (ASCII character set). Using non-ASCII characters returns an error. Examples of invalid, non-ASCII characters are Chinese, Japanese kanjis, and emojis. - sql_params: - description: 'A map from keys to values for jobs with SQL task, for example `"sql_params": - {"name": "john doe", "age": "35"}`. The SQL alert task does not support custom - parameters.' + "sql_params": + "description": |- + A map from keys to values for jobs with SQL task, for example `"sql_params": {"name": "john doe", "age": "35"}`. The SQL alert task does not support custom parameters. github.com/databricks/databricks-sdk-go/service/jobs.Source: - _: - description: |- + "_": + "description": |- Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\ from the local Databricks workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository defined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise. * `WORKSPACE`: SQL file is located in Databricks workspace. * `GIT`: SQL file is located in cloud Git provider. - enum: - - WORKSPACE - - GIT + "enum": + - |- + WORKSPACE + - |- + GIT github.com/databricks/databricks-sdk-go/service/jobs.SparkJarTask: - jar_uri: - description: Deprecated since 04/2016. Provide a `jar` through the `libraries` - field instead. For an example, see :method:jobs/create. - main_class_name: - description: |- + "jar_uri": + "description": |- + Deprecated since 04/2016. Provide a `jar` through the `libraries` field instead. For an example, see :method:jobs/create. + "main_class_name": + "description": |- The full name of the class containing the main method to be executed. This class must be contained in a JAR provided as a library. The code must use `SparkContext.getOrCreate` to obtain a Spark context; otherwise, runs of the job fail. - parameters: - description: |- + "parameters": + "description": |- Parameters passed to the main method. Use [Task parameter variables](https://docs.databricks.com/jobs.html#parameter-variables) to set parameters containing information about job runs. github.com/databricks/databricks-sdk-go/service/jobs.SparkPythonTask: - parameters: - description: |- + "parameters": + "description": |- Command line parameters passed to the Python file. Use [Task parameter variables](https://docs.databricks.com/jobs.html#parameter-variables) to set parameters containing information about job runs. - python_file: - description: The Python file to be executed. Cloud file URIs (such as dbfs:/, - s3:/, adls:/, gcs:/) and workspace paths are supported. For python files stored - in the Databricks workspace, the path must be absolute and begin with `/`. For - files stored in a remote repository, the path must be relative. This field is - required. - source: - description: |- + "python_file": + "description": |- + The Python file to be executed. Cloud file URIs (such as dbfs:/, s3:/, adls:/, gcs:/) and workspace paths are supported. For python files stored in the Databricks workspace, the path must be absolute and begin with `/`. For files stored in a remote repository, the path must be relative. This field is required. + "source": + "description": |- Optional location type of the Python file. When set to `WORKSPACE` or not specified, the file will be retrieved from the local Databricks workspace or cloud location (if the `python_file` has a URI format). When set to `GIT`, the Python file will be retrieved from a Git repository defined in `git_source`. @@ -1613,52 +1766,59 @@ github.com/databricks/databricks-sdk-go/service/jobs.SparkPythonTask: * `WORKSPACE`: The Python file is located in a Databricks workspace or at a cloud filesystem URI. * `GIT`: The Python file is located in a remote Git repository. github.com/databricks/databricks-sdk-go/service/jobs.SparkSubmitTask: - parameters: - description: |- + "parameters": + "description": |- Command-line parameters passed to spark submit. Use [Task parameter variables](https://docs.databricks.com/jobs.html#parameter-variables) to set parameters containing information about job runs. github.com/databricks/databricks-sdk-go/service/jobs.SqlTask: - alert: - description: If alert, indicates that this job must refresh a SQL alert. - dashboard: - description: If dashboard, indicates that this job must refresh a SQL dashboard. - file: - description: If file, indicates that this job runs a SQL file in a remote Git - repository. - parameters: - description: Parameters to be used for each run of this job. The SQL alert task - does not support custom parameters. - query: - description: If query, indicates that this job must execute a SQL query. - warehouse_id: - description: The canonical identifier of the SQL warehouse. Recommended to use - with serverless or pro SQL warehouses. Classic SQL warehouses are only supported - for SQL alert, dashboard and query tasks and are limited to scheduled single-task - jobs. + "alert": + "description": |- + If alert, indicates that this job must refresh a SQL alert. + "dashboard": + "description": |- + If dashboard, indicates that this job must refresh a SQL dashboard. + "file": + "description": |- + If file, indicates that this job runs a SQL file in a remote Git repository. + "parameters": + "description": |- + Parameters to be used for each run of this job. The SQL alert task does not support custom parameters. + "query": + "description": |- + If query, indicates that this job must execute a SQL query. + "warehouse_id": + "description": |- + The canonical identifier of the SQL warehouse. Recommended to use with serverless or pro SQL warehouses. Classic SQL warehouses are only supported for SQL alert, dashboard and query tasks and are limited to scheduled single-task jobs. github.com/databricks/databricks-sdk-go/service/jobs.SqlTaskAlert: - alert_id: - description: The canonical identifier of the SQL alert. - pause_subscriptions: - description: If true, the alert notifications are not sent to subscribers. - subscriptions: - description: If specified, alert notifications are sent to subscribers. + "alert_id": + "description": |- + The canonical identifier of the SQL alert. + "pause_subscriptions": + "description": |- + If true, the alert notifications are not sent to subscribers. + "subscriptions": + "description": |- + If specified, alert notifications are sent to subscribers. github.com/databricks/databricks-sdk-go/service/jobs.SqlTaskDashboard: - custom_subject: - description: Subject of the email sent to subscribers of this task. - dashboard_id: - description: The canonical identifier of the SQL dashboard. - pause_subscriptions: - description: If true, the dashboard snapshot is not taken, and emails are not - sent to subscribers. - subscriptions: - description: If specified, dashboard snapshots are sent to subscriptions. + "custom_subject": + "description": |- + Subject of the email sent to subscribers of this task. + "dashboard_id": + "description": |- + The canonical identifier of the SQL dashboard. + "pause_subscriptions": + "description": |- + If true, the dashboard snapshot is not taken, and emails are not sent to subscribers. + "subscriptions": + "description": |- + If specified, dashboard snapshots are sent to subscriptions. github.com/databricks/databricks-sdk-go/service/jobs.SqlTaskFile: - path: - description: Path of the SQL file. Must be relative if the source is a remote - Git repository and absolute for workspace paths. - source: - description: |- + "path": + "description": |- + Path of the SQL file. Must be relative if the source is a remote Git repository and absolute for workspace paths. + "source": + "description": |- Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved from the local Databricks workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository defined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise. @@ -1666,107 +1826,104 @@ github.com/databricks/databricks-sdk-go/service/jobs.SqlTaskFile: * `WORKSPACE`: SQL file is located in Databricks workspace. * `GIT`: SQL file is located in cloud Git provider. github.com/databricks/databricks-sdk-go/service/jobs.SqlTaskQuery: - query_id: - description: The canonical identifier of the SQL query. + "query_id": + "description": |- + The canonical identifier of the SQL query. github.com/databricks/databricks-sdk-go/service/jobs.SqlTaskSubscription: - destination_id: - description: The canonical identifier of the destination to receive email notification. - This parameter is mutually exclusive with user_name. You cannot set both destination_id - and user_name for subscription notifications. - user_name: - description: The user name to receive the subscription email. This parameter is - mutually exclusive with destination_id. You cannot set both destination_id and - user_name for subscription notifications. + "destination_id": + "description": |- + The canonical identifier of the destination to receive email notification. This parameter is mutually exclusive with user_name. You cannot set both destination_id and user_name for subscription notifications. + "user_name": + "description": |- + The user name to receive the subscription email. This parameter is mutually exclusive with destination_id. You cannot set both destination_id and user_name for subscription notifications. github.com/databricks/databricks-sdk-go/service/jobs.TableUpdateTriggerConfiguration: - condition: - description: The table(s) condition based on which to trigger a job run. - min_time_between_triggers_seconds: - description: |- + "condition": + "description": |- + The table(s) condition based on which to trigger a job run. + "min_time_between_triggers_seconds": + "description": |- If set, the trigger starts a run only after the specified amount of time has passed since the last time the trigger fired. The minimum allowed value is 60 seconds. - table_names: - description: A list of Delta tables to monitor for changes. The table name must - be in the format `catalog_name.schema_name.table_name`. - wait_after_last_change_seconds: - description: |- + "table_names": + "description": |- + A list of Delta tables to monitor for changes. The table name must be in the format `catalog_name.schema_name.table_name`. + "wait_after_last_change_seconds": + "description": |- If set, the trigger starts a run only after no table updates have occurred for the specified time and can be used to wait for a series of table updates before triggering a run. The minimum allowed value is 60 seconds. github.com/databricks/databricks-sdk-go/service/jobs.Task: - clean_rooms_notebook_task: - description: |- + "clean_rooms_notebook_task": + "description": |- The task runs a [clean rooms](https://docs.databricks.com/en/clean-rooms/index.html) notebook when the `clean_rooms_notebook_task` field is present. - condition_task: - description: |- + "condition_task": + "description": |- The task evaluates a condition that can be used to control the execution of other tasks when the `condition_task` field is present. The condition task does not require a cluster to execute and does not support retries or notifications. - dbt_task: - description: The task runs one or more dbt commands when the `dbt_task` field - is present. The dbt task requires both Databricks SQL and the ability to use - a serverless or a pro SQL warehouse. - depends_on: - description: |- + "dbt_task": + "description": |- + The task runs one or more dbt commands when the `dbt_task` field is present. The dbt task requires both Databricks SQL and the ability to use a serverless or a pro SQL warehouse. + "depends_on": + "description": |- An optional array of objects specifying the dependency graph of the task. All tasks specified in this field must complete before executing this task. The task will run only if the `run_if` condition is true. The key is `task_key`, and the value is the name assigned to the dependent task. - description: - description: An optional description for this task. - disable_auto_optimization: - description: An option to disable auto optimization in serverless - email_notifications: - description: An optional set of email addresses that is notified when runs of - this task begin or complete as well as when this task is deleted. The default - behavior is to not send any emails. - environment_key: - description: The key that references an environment spec in a job. This field - is required for Python script, Python wheel and dbt tasks when using serverless - compute. - existing_cluster_id: - description: |- + "description": + "description": |- + An optional description for this task. + "disable_auto_optimization": + "description": |- + An option to disable auto optimization in serverless + "email_notifications": + "description": |- + An optional set of email addresses that is notified when runs of this task begin or complete as well as when this task is deleted. The default behavior is to not send any emails. + "environment_key": + "description": |- + The key that references an environment spec in a job. This field is required for Python script, Python wheel and dbt tasks when using serverless compute. + "existing_cluster_id": + "description": |- If existing_cluster_id, the ID of an existing cluster that is used for all runs. When running jobs or tasks on an existing cluster, you may need to manually restart the cluster if it stops responding. We suggest running jobs and tasks on new clusters for greater reliability - for_each_task: - description: The task executes a nested task for every input provided when the - `for_each_task` field is present. - health: {} - job_cluster_key: - description: If job_cluster_key, this task is executed reusing the cluster specified - in `job.settings.job_clusters`. - libraries: - description: |- + "for_each_task": + "description": |- + The task executes a nested task for every input provided when the `for_each_task` field is present. + "health": {} + "job_cluster_key": + "description": |- + If job_cluster_key, this task is executed reusing the cluster specified in `job.settings.job_clusters`. + "libraries": + "description": |- An optional list of libraries to be installed on the cluster. The default value is an empty list. - max_retries: - description: An optional maximum number of times to retry an unsuccessful run. - A run is considered to be unsuccessful if it completes with the `FAILED` result_state - or `INTERNAL_ERROR` `life_cycle_state`. The value `-1` means to retry indefinitely - and the value `0` means to never retry. - min_retry_interval_millis: - description: An optional minimal interval in milliseconds between the start of - the failed run and the subsequent retry run. The default behavior is that unsuccessful - runs are immediately retried. - new_cluster: - description: If new_cluster, a description of a new cluster that is created for - each run. - notebook_task: - description: The task runs a notebook when the `notebook_task` field is present. - notification_settings: - description: Optional notification settings that are used when sending notifications - to each of the `email_notifications` and `webhook_notifications` for this task. - pipeline_task: - description: The task triggers a pipeline update when the `pipeline_task` field - is present. Only pipelines configured to use triggered more are supported. - python_wheel_task: - description: The task runs a Python wheel when the `python_wheel_task` field is - present. - retry_on_timeout: - description: |- + "max_retries": + "description": |- + An optional maximum number of times to retry an unsuccessful run. A run is considered to be unsuccessful if it completes with the `FAILED` result_state or `INTERNAL_ERROR` `life_cycle_state`. The value `-1` means to retry indefinitely and the value `0` means to never retry. + "min_retry_interval_millis": + "description": |- + An optional minimal interval in milliseconds between the start of the failed run and the subsequent retry run. The default behavior is that unsuccessful runs are immediately retried. + "new_cluster": + "description": |- + If new_cluster, a description of a new cluster that is created for each run. + "notebook_task": + "description": |- + The task runs a notebook when the `notebook_task` field is present. + "notification_settings": + "description": |- + Optional notification settings that are used when sending notifications to each of the `email_notifications` and `webhook_notifications` for this task. + "pipeline_task": + "description": |- + The task triggers a pipeline update when the `pipeline_task` field is present. Only pipelines configured to use triggered more are supported. + "python_wheel_task": + "description": |- + The task runs a Python wheel when the `python_wheel_task` field is present. + "retry_on_timeout": + "description": |- An optional policy to specify whether to retry a job when it times out. The default behavior is to not retry on timeout. - run_if: - description: |- + "run_if": + "description": |- An optional value specifying the condition determining whether the task is run once its dependencies have been completed. * `ALL_SUCCESS`: All dependencies have executed and succeeded @@ -1775,15 +1932,17 @@ github.com/databricks/databricks-sdk-go/service/jobs.Task: * `ALL_DONE`: All dependencies have been completed * `AT_LEAST_ONE_FAILED`: At least one dependency failed * `ALL_FAILED`: ALl dependencies have failed - run_job_task: - description: The task triggers another job when the `run_job_task` field is present. - spark_jar_task: - description: The task runs a JAR when the `spark_jar_task` field is present. - spark_python_task: - description: The task runs a Python file when the `spark_python_task` field is - present. - spark_submit_task: - description: |- + "run_job_task": + "description": |- + The task triggers another job when the `run_job_task` field is present. + "spark_jar_task": + "description": |- + The task runs a JAR when the `spark_jar_task` field is present. + "spark_python_task": + "description": |- + The task runs a Python file when the `spark_python_task` field is present. + "spark_submit_task": + "description": |- (Legacy) The task runs the spark-submit script when the `spark_submit_task` field is present. This task can run only on new clusters and is not compatible with serverless compute. In the `new_cluster` specification, `libraries` and `spark_conf` are not supported. Instead, use `--jars` and `--py-files` to add Java and Python libraries and `--conf` to set the Spark configurations. @@ -1793,220 +1952,238 @@ github.com/databricks/databricks-sdk-go/service/jobs.Task: By default, the Spark submit job uses all available memory (excluding reserved memory for Databricks services). You can set `--driver-memory`, and `--executor-memory` to a smaller value to leave some room for off-heap usage. The `--jars`, `--py-files`, `--files` arguments support DBFS and S3 paths. - sql_task: - description: The task runs a SQL query or file, or it refreshes a SQL alert or - a legacy SQL dashboard when the `sql_task` field is present. - task_key: - description: |- + "sql_task": + "description": |- + The task runs a SQL query or file, or it refreshes a SQL alert or a legacy SQL dashboard when the `sql_task` field is present. + "task_key": + "description": |- A unique name for the task. This field is used to refer to this task from other tasks. This field is required and must be unique within its parent job. On Update or Reset, this field is used to reference the tasks to be updated or reset. - timeout_seconds: - description: An optional timeout applied to each run of this job task. A value - of `0` means no timeout. - webhook_notifications: - description: A collection of system notification IDs to notify when runs of this - task begin or complete. The default behavior is to not send any system notifications. + "timeout_seconds": + "description": |- + An optional timeout applied to each run of this job task. A value of `0` means no timeout. + "webhook_notifications": + "description": |- + A collection of system notification IDs to notify when runs of this task begin or complete. The default behavior is to not send any system notifications. github.com/databricks/databricks-sdk-go/service/jobs.TaskDependency: - outcome: - description: Can only be specified on condition task dependencies. The outcome - of the dependent task that must be met for this task to run. - task_key: - description: The name of the task this task depends on. + "outcome": + "description": |- + Can only be specified on condition task dependencies. The outcome of the dependent task that must be met for this task to run. + "task_key": + "description": |- + The name of the task this task depends on. github.com/databricks/databricks-sdk-go/service/jobs.TaskEmailNotifications: - no_alert_for_skipped_runs: - description: |- + "no_alert_for_skipped_runs": + "description": |- If true, do not send email to recipients specified in `on_failure` if the run is skipped. This field is `deprecated`. Please use the `notification_settings.no_alert_for_skipped_runs` field. - on_duration_warning_threshold_exceeded: - description: A list of email addresses to be notified when the duration of a run - exceeds the threshold specified for the `RUN_DURATION_SECONDS` metric in the - `health` field. If no rule for the `RUN_DURATION_SECONDS` metric is specified - in the `health` field for the job, notifications are not sent. - on_failure: - description: A list of email addresses to be notified when a run unsuccessfully - completes. A run is considered to have completed unsuccessfully if it ends with - an `INTERNAL_ERROR` `life_cycle_state` or a `FAILED`, or `TIMED_OUT` result_state. - If this is not specified on job creation, reset, or update the list is empty, - and notifications are not sent. - on_start: - description: A list of email addresses to be notified when a run begins. If not - specified on job creation, reset, or update, the list is empty, and notifications - are not sent. - on_streaming_backlog_exceeded: - description: |- + "on_duration_warning_threshold_exceeded": + "description": |- + A list of email addresses to be notified when the duration of a run exceeds the threshold specified for the `RUN_DURATION_SECONDS` metric in the `health` field. If no rule for the `RUN_DURATION_SECONDS` metric is specified in the `health` field for the job, notifications are not sent. + "on_failure": + "description": |- + A list of email addresses to be notified when a run unsuccessfully completes. A run is considered to have completed unsuccessfully if it ends with an `INTERNAL_ERROR` `life_cycle_state` or a `FAILED`, or `TIMED_OUT` result_state. If this is not specified on job creation, reset, or update the list is empty, and notifications are not sent. + "on_start": + "description": |- + A list of email addresses to be notified when a run begins. If not specified on job creation, reset, or update, the list is empty, and notifications are not sent. + "on_streaming_backlog_exceeded": + "description": |- A list of email addresses to notify when any streaming backlog thresholds are exceeded for any stream. Streaming backlog thresholds can be set in the `health` field using the following metrics: `STREAMING_BACKLOG_BYTES`, `STREAMING_BACKLOG_RECORDS`, `STREAMING_BACKLOG_SECONDS`, or `STREAMING_BACKLOG_FILES`. Alerting is based on the 10-minute average of these metrics. If the issue persists, notifications are resent every 30 minutes. - on_success: - description: A list of email addresses to be notified when a run successfully - completes. A run is considered to have completed successfully if it ends with - a `TERMINATED` `life_cycle_state` and a `SUCCESS` result_state. If not specified - on job creation, reset, or update, the list is empty, and notifications are - not sent. + "on_success": + "description": |- + A list of email addresses to be notified when a run successfully completes. A run is considered to have completed successfully if it ends with a `TERMINATED` `life_cycle_state` and a `SUCCESS` result_state. If not specified on job creation, reset, or update, the list is empty, and notifications are not sent. github.com/databricks/databricks-sdk-go/service/jobs.TaskNotificationSettings: - alert_on_last_attempt: - description: If true, do not send notifications to recipients specified in `on_start` - for the retried runs and do not send notifications to recipients specified in - `on_failure` until the last retry of the run. - no_alert_for_canceled_runs: - description: If true, do not send notifications to recipients specified in `on_failure` - if the run is canceled. - no_alert_for_skipped_runs: - description: If true, do not send notifications to recipients specified in `on_failure` - if the run is skipped. + "alert_on_last_attempt": + "description": |- + If true, do not send notifications to recipients specified in `on_start` for the retried runs and do not send notifications to recipients specified in `on_failure` until the last retry of the run. + "no_alert_for_canceled_runs": + "description": |- + If true, do not send notifications to recipients specified in `on_failure` if the run is canceled. + "no_alert_for_skipped_runs": + "description": |- + If true, do not send notifications to recipients specified in `on_failure` if the run is skipped. github.com/databricks/databricks-sdk-go/service/jobs.TriggerSettings: - file_arrival: - description: File arrival trigger settings. - pause_status: - description: Whether this trigger is paused or not. - periodic: - description: Periodic trigger settings. - table: - description: Old table trigger settings name. Deprecated in favor of `table_update`. - table_update: {} + "file_arrival": + "description": |- + File arrival trigger settings. + "pause_status": + "description": |- + Whether this trigger is paused or not. + "periodic": + "description": |- + Periodic trigger settings. + "table": + "description": |- + Old table trigger settings name. Deprecated in favor of `table_update`. + "table_update": {} github.com/databricks/databricks-sdk-go/service/jobs.Webhook: - id: {} + "id": {} github.com/databricks/databricks-sdk-go/service/jobs.WebhookNotifications: - on_duration_warning_threshold_exceeded: - description: An optional list of system notification IDs to call when the duration - of a run exceeds the threshold specified for the `RUN_DURATION_SECONDS` metric - in the `health` field. A maximum of 3 destinations can be specified for the - `on_duration_warning_threshold_exceeded` property. - on_failure: - description: An optional list of system notification IDs to call when the run - fails. A maximum of 3 destinations can be specified for the `on_failure` property. - on_start: - description: An optional list of system notification IDs to call when the run - starts. A maximum of 3 destinations can be specified for the `on_start` property. - on_streaming_backlog_exceeded: - description: |- + "on_duration_warning_threshold_exceeded": + "description": |- + An optional list of system notification IDs to call when the duration of a run exceeds the threshold specified for the `RUN_DURATION_SECONDS` metric in the `health` field. A maximum of 3 destinations can be specified for the `on_duration_warning_threshold_exceeded` property. + "on_failure": + "description": |- + An optional list of system notification IDs to call when the run fails. A maximum of 3 destinations can be specified for the `on_failure` property. + "on_start": + "description": |- + An optional list of system notification IDs to call when the run starts. A maximum of 3 destinations can be specified for the `on_start` property. + "on_streaming_backlog_exceeded": + "description": |- An optional list of system notification IDs to call when any streaming backlog thresholds are exceeded for any stream. Streaming backlog thresholds can be set in the `health` field using the following metrics: `STREAMING_BACKLOG_BYTES`, `STREAMING_BACKLOG_RECORDS`, `STREAMING_BACKLOG_SECONDS`, or `STREAMING_BACKLOG_FILES`. Alerting is based on the 10-minute average of these metrics. If the issue persists, notifications are resent every 30 minutes. A maximum of 3 destinations can be specified for the `on_streaming_backlog_exceeded` property. - on_success: - description: An optional list of system notification IDs to call when the run - completes successfully. A maximum of 3 destinations can be specified for the - `on_success` property. + "on_success": + "description": |- + An optional list of system notification IDs to call when the run completes successfully. A maximum of 3 destinations can be specified for the `on_success` property. github.com/databricks/databricks-sdk-go/service/ml.ExperimentTag: - key: - description: The tag key. - value: - description: The tag value. + "key": + "description": |- + The tag key. + "value": + "description": |- + The tag value. github.com/databricks/databricks-sdk-go/service/ml.ModelTag: - key: - description: The tag key. - value: - description: The tag value. + "key": + "description": |- + The tag key. + "value": + "description": |- + The tag value. github.com/databricks/databricks-sdk-go/service/ml.ModelVersion: - creation_timestamp: - description: Timestamp recorded when this `model_version` was created. - current_stage: - description: Current stage for this `model_version`. - description: - description: Description of this `model_version`. - last_updated_timestamp: - description: Timestamp recorded when metadata for this `model_version` was last - updated. - name: - description: Unique name of the model - run_id: - description: |- + "creation_timestamp": + "description": |- + Timestamp recorded when this `model_version` was created. + "current_stage": + "description": |- + Current stage for this `model_version`. + "description": + "description": |- + Description of this `model_version`. + "last_updated_timestamp": + "description": |- + Timestamp recorded when metadata for this `model_version` was last updated. + "name": + "description": |- + Unique name of the model + "run_id": + "description": |- MLflow run ID used when creating `model_version`, if `source` was generated by an experiment run stored in MLflow tracking server. - run_link: - description: 'Run Link: Direct link to the run that generated this version' - source: - description: URI indicating the location of the source model artifacts, used when - creating `model_version` - status: - description: Current status of `model_version` - status_message: - description: Details on current `status`, if it is pending or failed. - tags: - description: 'Tags: Additional metadata key-value pairs for this `model_version`.' - user_id: - description: User that created this `model_version`. - version: - description: Model's version number. + "run_link": + "description": |- + Run Link: Direct link to the run that generated this version + "source": + "description": |- + URI indicating the location of the source model artifacts, used when creating `model_version` + "status": + "description": |- + Current status of `model_version` + "status_message": + "description": |- + Details on current `status`, if it is pending or failed. + "tags": + "description": |- + Tags: Additional metadata key-value pairs for this `model_version`. + "user_id": + "description": |- + User that created this `model_version`. + "version": + "description": |- + Model's version number. github.com/databricks/databricks-sdk-go/service/ml.ModelVersionStatus: - _: - description: Current status of `model_version` - enum: - - PENDING_REGISTRATION - - FAILED_REGISTRATION - - READY + "_": + "description": |- + Current status of `model_version` + "enum": + - |- + PENDING_REGISTRATION + - |- + FAILED_REGISTRATION + - |- + READY github.com/databricks/databricks-sdk-go/service/ml.ModelVersionTag: - key: - description: The tag key. - value: - description: The tag value. + "key": + "description": |- + The tag key. + "value": + "description": |- + The tag value. github.com/databricks/databricks-sdk-go/service/pipelines.CronTrigger: - quartz_cron_schedule: {} - timezone_id: {} + "quartz_cron_schedule": {} + "timezone_id": {} github.com/databricks/databricks-sdk-go/service/pipelines.DeploymentKind: - _: - description: | + "_": + "description": | The deployment method that manages the pipeline: - BUNDLE: The pipeline is managed by a Databricks Asset Bundle. - enum: - - BUNDLE + "enum": + - |- + BUNDLE github.com/databricks/databricks-sdk-go/service/pipelines.FileLibrary: - path: - description: The absolute path of the file. + "path": + "description": |- + The absolute path of the file. github.com/databricks/databricks-sdk-go/service/pipelines.Filters: - exclude: - description: Paths to exclude. - include: - description: Paths to include. + "exclude": + "description": |- + Paths to exclude. + "include": + "description": |- + Paths to include. github.com/databricks/databricks-sdk-go/service/pipelines.IngestionConfig: - report: - description: Select a specific source report. - schema: - description: Select all tables from a specific source schema. - table: - description: Select a specific source table. + "report": + "description": |- + Select a specific source report. + "schema": + "description": |- + Select all tables from a specific source schema. + "table": + "description": |- + Select a specific source table. github.com/databricks/databricks-sdk-go/service/pipelines.IngestionGatewayPipelineDefinition: - connection_id: - description: '[Deprecated, use connection_name instead] Immutable. The Unity Catalog - connection that this gateway pipeline uses to communicate with the source.' - connection_name: - description: Immutable. The Unity Catalog connection that this gateway pipeline - uses to communicate with the source. - gateway_storage_catalog: - description: Required, Immutable. The name of the catalog for the gateway pipeline's - storage location. - gateway_storage_name: - description: | + "connection_id": + "description": |- + [Deprecated, use connection_name instead] Immutable. The Unity Catalog connection that this gateway pipeline uses to communicate with the source. + "connection_name": + "description": |- + Immutable. The Unity Catalog connection that this gateway pipeline uses to communicate with the source. + "gateway_storage_catalog": + "description": |- + Required, Immutable. The name of the catalog for the gateway pipeline's storage location. + "gateway_storage_name": + "description": | Optional. The Unity Catalog-compatible name for the gateway storage location. This is the destination to use for the data that is extracted by the gateway. Delta Live Tables system will automatically create the storage location under the catalog and schema. - gateway_storage_schema: - description: Required, Immutable. The name of the schema for the gateway pipelines's - storage location. + "gateway_storage_schema": + "description": |- + Required, Immutable. The name of the schema for the gateway pipelines's storage location. github.com/databricks/databricks-sdk-go/service/pipelines.IngestionPipelineDefinition: - connection_name: - description: Immutable. The Unity Catalog connection that this ingestion pipeline - uses to communicate with the source. This is used with connectors for applications - like Salesforce, Workday, and so on. - ingestion_gateway_id: - description: Immutable. Identifier for the gateway that is used by this ingestion - pipeline to communicate with the source database. This is used with connectors - to databases like SQL Server. - objects: - description: Required. Settings specifying tables to replicate and the destination - for the replicated tables. - table_configuration: - description: Configuration settings to control the ingestion of tables. These - settings are applied to all tables in the pipeline. + "connection_name": + "description": |- + Immutable. The Unity Catalog connection that this ingestion pipeline uses to communicate with the source. This is used with connectors for applications like Salesforce, Workday, and so on. + "ingestion_gateway_id": + "description": |- + Immutable. Identifier for the gateway that is used by this ingestion pipeline to communicate with the source database. This is used with connectors to databases like SQL Server. + "objects": + "description": |- + Required. Settings specifying tables to replicate and the destination for the replicated tables. + "table_configuration": + "description": |- + Configuration settings to control the ingestion of tables. These settings are applied to all tables in the pipeline. github.com/databricks/databricks-sdk-go/service/pipelines.ManualTrigger: {} github.com/databricks/databricks-sdk-go/service/pipelines.NotebookLibrary: - path: - description: The absolute path of the notebook. + "path": + "description": |- + The absolute path of the notebook. github.com/databricks/databricks-sdk-go/service/pipelines.Notifications: - alerts: - description: | + "alerts": + "description": | A list of alerts that trigger the sending of notifications to the configured destinations. The supported alerts are: @@ -2014,74 +2191,74 @@ github.com/databricks/databricks-sdk-go/service/pipelines.Notifications: * `on-update-failure`: Each time a pipeline update fails. * `on-update-fatal-failure`: A pipeline update fails with a non-retryable (fatal) error. * `on-flow-failure`: A single data flow fails. - email_recipients: - description: | + "email_recipients": + "description": | A list of email addresses notified when a configured alert is triggered. github.com/databricks/databricks-sdk-go/service/pipelines.PipelineCluster: - apply_policy_default_values: - description: 'Note: This field won''t be persisted. Only API users will check - this field.' - autoscale: - description: |- + "apply_policy_default_values": + "description": |- + Note: This field won't be persisted. Only API users will check this field. + "autoscale": + "description": |- Parameters needed in order to automatically scale clusters up and down based on load. Note: autoscaling works best with DB runtime versions 3.0 or later. - aws_attributes: - description: |- + "aws_attributes": + "description": |- Attributes related to clusters running on Amazon Web Services. If not specified at cluster creation, a set of default values will be used. - azure_attributes: - description: |- + "azure_attributes": + "description": |- Attributes related to clusters running on Microsoft Azure. If not specified at cluster creation, a set of default values will be used. - cluster_log_conf: - description: | + "cluster_log_conf": + "description": | The configuration for delivering spark logs to a long-term storage destination. Only dbfs destinations are supported. Only one destination can be specified for one cluster. If the conf is given, the logs will be delivered to the destination every `5 mins`. The destination of driver logs is `$destination/$clusterId/driver`, while the destination of executor logs is `$destination/$clusterId/executor`. - custom_tags: - description: |- + "custom_tags": + "description": |- Additional tags for cluster resources. Databricks will tag all cluster resources (e.g., AWS instances and EBS volumes) with these tags in addition to `default_tags`. Notes: - Currently, Databricks allows at most 45 custom tags - Clusters can only reuse cloud resources if the resources' tags are a subset of the cluster tags - driver_instance_pool_id: - description: |- + "driver_instance_pool_id": + "description": |- The optional ID of the instance pool for the driver of the cluster belongs. The pool cluster uses the instance pool with id (instance_pool_id) if the driver pool is not assigned. - driver_node_type_id: - description: |- + "driver_node_type_id": + "description": |- The node type of the Spark driver. Note that this field is optional; if unset, the driver node type will be set as the same value as `node_type_id` defined above. - enable_local_disk_encryption: - description: Whether to enable local disk encryption for the cluster. - gcp_attributes: - description: |- + "enable_local_disk_encryption": + "description": |- + Whether to enable local disk encryption for the cluster. + "gcp_attributes": + "description": |- Attributes related to clusters running on Google Cloud Platform. If not specified at cluster creation, a set of default values will be used. - 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 `//init_scripts`. - instance_pool_id: - description: The optional ID of the instance pool to which the cluster belongs. - label: - description: A label for the cluster specification, either `default` to configure - the default cluster, or `maintenance` to configure the maintenance cluster. - This field is optional. The default value is `default`. - node_type_id: - description: | + "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 `//init_scripts`. + "instance_pool_id": + "description": |- + The optional ID of the instance pool to which the cluster belongs. + "label": + "description": |- + A label for the cluster specification, either `default` to configure the default cluster, or `maintenance` to configure the maintenance cluster. This field is optional. The default value is `default`. + "node_type_id": + "description": | This field encodes, through a single value, the resources available to each of the Spark nodes in this cluster. For example, the Spark nodes can be provisioned and optimized for memory or compute intensive workloads. A list of available node types can be retrieved by using the :method:clusters/listNodeTypes API call. - num_workers: - description: |- + "num_workers": + "description": |- Number of worker nodes that this cluster should have. A cluster has one Spark Driver and `num_workers` Executors for a total of `num_workers` + 1 Spark nodes. @@ -2090,14 +2267,15 @@ github.com/databricks/databricks-sdk-go/service/pipelines.PipelineCluster: is resized from 5 to 10 workers, this field will immediately be updated to reflect the target size of 10 workers, whereas the workers listed in `spark_info` will gradually increase from 5 to 10 as the new nodes are provisioned. - policy_id: - description: The ID of the cluster policy used to create the cluster if applicable. - spark_conf: - description: | + "policy_id": + "description": |- + The ID of the cluster policy used to create the cluster if applicable. + "spark_conf": + "description": | An object containing a set of optional, user-specified Spark configuration key-value pairs. See :method:clusters/create for more details. - spark_env_vars: - description: |- + "spark_env_vars": + "description": |- An object containing a set of optional, user-specified environment variable key-value pairs. Please note that key-value pair of the form (X,Y) will be exported as is (i.e., `export X='Y'`) while launching the driver and workers. @@ -2109,523 +2287,638 @@ github.com/databricks/databricks-sdk-go/service/pipelines.PipelineCluster: Example Spark environment variables: `{"SPARK_WORKER_MEMORY": "28000m", "SPARK_LOCAL_DIRS": "/local_disk0"}` or `{"SPARK_DAEMON_JAVA_OPTS": "$SPARK_DAEMON_JAVA_OPTS -Dspark.shuffle.service.enabled=true"}` - ssh_public_keys: - description: |- + "ssh_public_keys": + "description": |- SSH public key contents that will be added to each Spark node in this cluster. The corresponding private keys can be used to login with the user name `ubuntu` on port `2200`. Up to 10 keys can be specified. github.com/databricks/databricks-sdk-go/service/pipelines.PipelineClusterAutoscale: - max_workers: - description: The maximum number of workers to which the cluster can scale up when - overloaded. `max_workers` must be strictly greater than `min_workers`. - min_workers: - description: |- + "max_workers": + "description": |- + The maximum number of workers to which the cluster can scale up when overloaded. `max_workers` must be strictly greater than `min_workers`. + "min_workers": + "description": |- The minimum number of workers the cluster can scale down to when underutilized. It is also the initial number of workers the cluster will have after creation. - mode: - description: | + "mode": + "description": | Databricks Enhanced Autoscaling optimizes cluster utilization by automatically allocating cluster resources based on workload volume, with minimal impact to the data processing latency of your pipelines. Enhanced Autoscaling is available for `updates` clusters only. The legacy autoscaling feature is used for `maintenance` clusters. github.com/databricks/databricks-sdk-go/service/pipelines.PipelineClusterAutoscaleMode: - _: - description: | + "_": + "description": | Databricks Enhanced Autoscaling optimizes cluster utilization by automatically allocating cluster resources based on workload volume, with minimal impact to the data processing latency of your pipelines. Enhanced Autoscaling is available for `updates` clusters only. The legacy autoscaling feature is used for `maintenance` clusters. - enum: - - ENHANCED - - LEGACY + "enum": + - |- + ENHANCED + - |- + LEGACY github.com/databricks/databricks-sdk-go/service/pipelines.PipelineDeployment: - kind: - description: The deployment method that manages the pipeline. - metadata_file_path: - description: The path to the file containing metadata about the deployment. + "kind": + "description": |- + The deployment method that manages the pipeline. + "metadata_file_path": + "description": |- + The path to the file containing metadata about the deployment. github.com/databricks/databricks-sdk-go/service/pipelines.PipelineLibrary: - file: - description: | + "file": + "description": | The path to a file that defines a pipeline and is stored in the Databricks Repos. - jar: - description: | + "jar": + "description": | URI of the jar to be installed. Currently only DBFS is supported. - maven: - description: | + "maven": + "description": | Specification of a maven library to be installed. - notebook: - description: | + "notebook": + "description": | The path to a notebook that defines a pipeline and is stored in the Databricks workspace. - whl: - description: URI of the whl to be installed. + "whl": + "description": |- + URI of the whl to be installed. github.com/databricks/databricks-sdk-go/service/pipelines.PipelineTrigger: - cron: {} - manual: {} + "cron": {} + "manual": {} github.com/databricks/databricks-sdk-go/service/pipelines.ReportSpec: - destination_catalog: - description: Required. Destination catalog to store table. - destination_schema: - description: Required. Destination schema to store table. - destination_table: - description: Required. Destination table name. The pipeline fails if a table with - that name already exists. - source_url: - description: Required. Report URL in the source system. - table_configuration: - description: Configuration settings to control the ingestion of tables. These - settings override the table_configuration defined in the IngestionPipelineDefinition - object. + "destination_catalog": + "description": |- + Required. Destination catalog to store table. + "destination_schema": + "description": |- + Required. Destination schema to store table. + "destination_table": + "description": |- + Required. Destination table name. The pipeline fails if a table with that name already exists. + "source_url": + "description": |- + Required. Report URL in the source system. + "table_configuration": + "description": |- + Configuration settings to control the ingestion of tables. These settings override the table_configuration defined in the IngestionPipelineDefinition object. github.com/databricks/databricks-sdk-go/service/pipelines.RestartWindow: - days_of_week: - description: |- + "days_of_week": + "description": |- Days of week in which the restart is allowed to happen (within a five-hour window starting at start_hour). If not specified all days of the week will be used. - start_hour: - description: |- + "start_hour": + "description": |- An integer between 0 and 23 denoting the start hour for the restart window in the 24-hour day. Continuous pipeline restart is triggered only within a five-hour window starting at this hour. - time_zone_id: - description: |- + "time_zone_id": + "description": |- Time zone id of restart window. See https://docs.databricks.com/sql/language-manual/sql-ref-syntax-aux-conf-mgmt-set-timezone.html for details. If not specified, UTC will be used. github.com/databricks/databricks-sdk-go/service/pipelines.RestartWindowDaysOfWeek: - _: - description: |- + "_": + "description": |- Days of week in which the restart is allowed to happen (within a five-hour window starting at start_hour). If not specified all days of the week will be used. - enum: - - MONDAY - - TUESDAY - - WEDNESDAY - - THURSDAY - - FRIDAY - - SATURDAY - - SUNDAY + "enum": + - |- + MONDAY + - |- + TUESDAY + - |- + WEDNESDAY + - |- + THURSDAY + - |- + FRIDAY + - |- + SATURDAY + - |- + SUNDAY github.com/databricks/databricks-sdk-go/service/pipelines.SchemaSpec: - destination_catalog: - description: Required. Destination catalog to store tables. - destination_schema: - description: Required. Destination schema to store tables in. Tables with the - same name as the source tables are created in this destination schema. The pipeline - fails If a table with the same name already exists. - source_catalog: - description: The source catalog name. Might be optional depending on the type - of source. - source_schema: - description: Required. Schema name in the source database. - table_configuration: - description: Configuration settings to control the ingestion of tables. These - settings are applied to all tables in this schema and override the table_configuration - defined in the IngestionPipelineDefinition object. + "destination_catalog": + "description": |- + Required. Destination catalog to store tables. + "destination_schema": + "description": |- + Required. Destination schema to store tables in. Tables with the same name as the source tables are created in this destination schema. The pipeline fails If a table with the same name already exists. + "source_catalog": + "description": |- + The source catalog name. Might be optional depending on the type of source. + "source_schema": + "description": |- + Required. Schema name in the source database. + "table_configuration": + "description": |- + Configuration settings to control the ingestion of tables. These settings are applied to all tables in this schema and override the table_configuration defined in the IngestionPipelineDefinition object. github.com/databricks/databricks-sdk-go/service/pipelines.TableSpec: - destination_catalog: - description: Required. Destination catalog to store table. - destination_schema: - description: Required. Destination schema to store table. - destination_table: - description: Optional. Destination table name. The pipeline fails if a table with - that name already exists. If not set, the source table name is used. - source_catalog: - description: Source catalog name. Might be optional depending on the type of source. - source_schema: - description: Schema name in the source database. Might be optional depending on - the type of source. - source_table: - description: Required. Table name in the source database. - table_configuration: - description: Configuration settings to control the ingestion of tables. These - settings override the table_configuration defined in the IngestionPipelineDefinition - object and the SchemaSpec. + "destination_catalog": + "description": |- + Required. Destination catalog to store table. + "destination_schema": + "description": |- + Required. Destination schema to store table. + "destination_table": + "description": |- + Optional. Destination table name. The pipeline fails if a table with that name already exists. If not set, the source table name is used. + "source_catalog": + "description": |- + Source catalog name. Might be optional depending on the type of source. + "source_schema": + "description": |- + Schema name in the source database. Might be optional depending on the type of source. + "source_table": + "description": |- + Required. Table name in the source database. + "table_configuration": + "description": |- + Configuration settings to control the ingestion of tables. These settings override the table_configuration defined in the IngestionPipelineDefinition object and the SchemaSpec. github.com/databricks/databricks-sdk-go/service/pipelines.TableSpecificConfig: - primary_keys: - description: The primary key of the table used to apply changes. - salesforce_include_formula_fields: - description: If true, formula fields defined in the table are included in the - ingestion. This setting is only valid for the Salesforce connector - scd_type: - description: The SCD type to use to ingest the table. - sequence_by: - description: The column names specifying the logical order of events in the source - data. Delta Live Tables uses this sequencing to handle change events that arrive - out of order. + "primary_keys": + "description": |- + The primary key of the table used to apply changes. + "salesforce_include_formula_fields": + "description": |- + If true, formula fields defined in the table are included in the ingestion. This setting is only valid for the Salesforce connector + "scd_type": + "description": |- + The SCD type to use to ingest the table. + "sequence_by": + "description": |- + The column names specifying the logical order of events in the source data. Delta Live Tables uses this sequencing to handle change events that arrive out of order. github.com/databricks/databricks-sdk-go/service/pipelines.TableSpecificConfigScdType: - _: - description: The SCD type to use to ingest the table. - enum: - - SCD_TYPE_1 - - SCD_TYPE_2 + "_": + "description": |- + The SCD type to use to ingest the table. + "enum": + - |- + SCD_TYPE_1 + - |- + SCD_TYPE_2 +github.com/databricks/databricks-sdk-go/service/serving.Ai21LabsConfig: + "ai21labs_api_key": + "description": |- + The Databricks secret key reference for an AI21 Labs API key. If you prefer to paste your API key directly, see `ai21labs_api_key_plaintext`. You must provide an API key using one of the following fields: `ai21labs_api_key` or `ai21labs_api_key_plaintext`. + "ai21labs_api_key_plaintext": + "description": |- + An AI21 Labs API key provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `ai21labs_api_key`. You must provide an API key using one of the following fields: `ai21labs_api_key` or `ai21labs_api_key_plaintext`. github.com/databricks/databricks-sdk-go/service/serving.AiGatewayConfig: - guardrails: - description: Configuration for AI Guardrails to prevent unwanted data and unsafe - data in requests and responses. - 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. - rate_limits: - description: Configuration for rate limits which can be set to limit endpoint - traffic. - 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. + "guardrails": + "description": |- + Configuration for AI Guardrails to prevent unwanted data and unsafe data in requests and responses. + "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. + "rate_limits": + "description": |- + Configuration for rate limits which can be set to limit endpoint traffic. + "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. github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrailParameters: - 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. - pii: - description: Configuration for guardrail PII filter. - safety: - description: Indicates whether the safety filter is enabled. - 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. + "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. + "pii": + "description": |- + Configuration for guardrail PII filter. + "safety": + "description": |- + Indicates whether the safety filter is enabled. + "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. github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrailPiiBehavior: - 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. + "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. github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrailPiiBehaviorBehavior: - _: - 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. - enum: - - NONE - - BLOCK + "_": + "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. + "enum": + - |- + NONE + - |- + BLOCK github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrails: - input: - description: Configuration for input guardrail filters. - output: - description: Configuration for output guardrail filters. + "input": + "description": |- + Configuration for input guardrail filters. + "output": + "description": |- + Configuration for output guardrail filters. github.com/databricks/databricks-sdk-go/service/serving.AiGatewayInferenceTableConfig: - 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.' - enabled: - description: Indicates whether the inference table is enabled. - 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.' - 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.' + "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. + "enabled": + "description": |- + Indicates whether the inference table is enabled. + "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. + "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. github.com/databricks/databricks-sdk-go/service/serving.AiGatewayRateLimit: - calls: - description: Used to specify how many calls are allowed for a key within the renewal_period. - key: - description: Key field for a rate limit. Currently, only 'user' and 'endpoint' - are supported, with 'endpoint' being the default if not specified. - renewal_period: - description: Renewal period field for a rate limit. Currently, only 'minute' is - supported. + "calls": + "description": |- + Used to specify how many calls are allowed for a key within the renewal_period. + "key": + "description": |- + Key field for a rate limit. Currently, only 'user' and 'endpoint' are supported, with 'endpoint' being the default if not specified. + "renewal_period": + "description": |- + Renewal period field for a rate limit. Currently, only 'minute' is supported. github.com/databricks/databricks-sdk-go/service/serving.AiGatewayRateLimitKey: - _: - description: Key field for a rate limit. Currently, only 'user' and 'endpoint' - are supported, with 'endpoint' being the default if not specified. - enum: - - user - - endpoint + "_": + "description": |- + Key field for a rate limit. Currently, only 'user' and 'endpoint' are supported, with 'endpoint' being the default if not specified. + "enum": + - |- + user + - |- + endpoint github.com/databricks/databricks-sdk-go/service/serving.AiGatewayRateLimitRenewalPeriod: - _: - description: Renewal period field for a rate limit. Currently, only 'minute' is - supported. - enum: - - minute + "_": + "description": |- + Renewal period field for a rate limit. Currently, only 'minute' is supported. + "enum": + - |- + minute github.com/databricks/databricks-sdk-go/service/serving.AiGatewayUsageTrackingConfig: - enabled: - description: Whether to enable usage tracking. + "enabled": + "description": |- + Whether to enable usage tracking. github.com/databricks/databricks-sdk-go/service/serving.AmazonBedrockConfig: - aws_access_key_id: - description: 'The Databricks secret key reference for an AWS access key ID with - permissions to interact with Bedrock services. If you prefer to paste your API - key directly, see `aws_access_key_id`. You must provide an API key using one - of the following fields: `aws_access_key_id` or `aws_access_key_id_plaintext`.' - aws_access_key_id_plaintext: - description: 'An AWS access key ID with permissions to interact with Bedrock services - provided as a plaintext string. If you prefer to reference your key using Databricks - Secrets, see `aws_access_key_id`. You must provide an API key using one of the - following fields: `aws_access_key_id` or `aws_access_key_id_plaintext`.' - aws_region: - description: The AWS region to use. Bedrock has to be enabled there. - aws_secret_access_key: - description: 'The Databricks secret key reference for an AWS secret access key - paired with the access key ID, with permissions to interact with Bedrock services. - If you prefer to paste your API key directly, see `aws_secret_access_key_plaintext`. - You must provide an API key using one of the following fields: `aws_secret_access_key` - or `aws_secret_access_key_plaintext`.' - aws_secret_access_key_plaintext: - description: 'An AWS secret access key paired with the access key ID, with permissions - to interact with Bedrock services provided as a plaintext string. If you prefer - to reference your key using Databricks Secrets, see `aws_secret_access_key`. - You must provide an API key using one of the following fields: `aws_secret_access_key` - or `aws_secret_access_key_plaintext`.' - bedrock_provider: - description: 'The underlying provider in Amazon Bedrock. Supported values (case - insensitive) include: Anthropic, Cohere, AI21Labs, Amazon.' + "aws_access_key_id": + "description": |- + The Databricks secret key reference for an AWS access key ID with permissions to interact with Bedrock services. If you prefer to paste your API key directly, see `aws_access_key_id`. You must provide an API key using one of the following fields: `aws_access_key_id` or `aws_access_key_id_plaintext`. + "aws_access_key_id_plaintext": + "description": |- + An AWS access key ID with permissions to interact with Bedrock services provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `aws_access_key_id`. You must provide an API key using one of the following fields: `aws_access_key_id` or `aws_access_key_id_plaintext`. + "aws_region": + "description": |- + The AWS region to use. Bedrock has to be enabled there. + "aws_secret_access_key": + "description": |- + The Databricks secret key reference for an AWS secret access key paired with the access key ID, with permissions to interact with Bedrock services. If you prefer to paste your API key directly, see `aws_secret_access_key_plaintext`. You must provide an API key using one of the following fields: `aws_secret_access_key` or `aws_secret_access_key_plaintext`. + "aws_secret_access_key_plaintext": + "description": |- + An AWS secret access key paired with the access key ID, with permissions to interact with Bedrock services provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `aws_secret_access_key`. You must provide an API key using one of the following fields: `aws_secret_access_key` or `aws_secret_access_key_plaintext`. + "bedrock_provider": + "description": |- + The underlying provider in Amazon Bedrock. Supported values (case insensitive) include: Anthropic, Cohere, AI21Labs, Amazon. github.com/databricks/databricks-sdk-go/service/serving.AmazonBedrockConfigBedrockProvider: - _: - description: 'The underlying provider in Amazon Bedrock. Supported values (case - insensitive) include: Anthropic, Cohere, AI21Labs, Amazon.' - enum: - - anthropic - - cohere - - ai21labs - - amazon + "_": + "description": |- + The underlying provider in Amazon Bedrock. Supported values (case insensitive) include: Anthropic, Cohere, AI21Labs, Amazon. + "enum": + - |- + anthropic + - |- + cohere + - |- + ai21labs + - |- + amazon github.com/databricks/databricks-sdk-go/service/serving.AnthropicConfig: - anthropic_api_key: - description: 'The Databricks secret key reference for an Anthropic API key. If - you prefer to paste your API key directly, see `anthropic_api_key_plaintext`. - You must provide an API key using one of the following fields: `anthropic_api_key` - or `anthropic_api_key_plaintext`.' - anthropic_api_key_plaintext: - description: 'The Anthropic API key provided as a plaintext string. If you prefer - to reference your key using Databricks Secrets, see `anthropic_api_key`. You - must provide an API key using one of the following fields: `anthropic_api_key` - or `anthropic_api_key_plaintext`.' + "anthropic_api_key": + "description": |- + The Databricks secret key reference for an Anthropic API key. If you prefer to paste your API key directly, see `anthropic_api_key_plaintext`. You must provide an API key using one of the following fields: `anthropic_api_key` or `anthropic_api_key_plaintext`. + "anthropic_api_key_plaintext": + "description": |- + The Anthropic API key provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `anthropic_api_key`. You must provide an API key using one of the following fields: `anthropic_api_key` or `anthropic_api_key_plaintext`. github.com/databricks/databricks-sdk-go/service/serving.AutoCaptureConfigInput: - catalog_name: - description: 'The name of the catalog in Unity Catalog. NOTE: On update, you cannot - change the catalog name if the inference table is already enabled.' - enabled: - description: Indicates whether the inference table is enabled. - schema_name: - description: 'The name of the schema in Unity Catalog. NOTE: On update, you cannot - change the schema name if the inference table is already enabled.' - table_name_prefix: - description: 'The prefix of the table in Unity Catalog. NOTE: On update, you cannot - change the prefix name if the inference table is already enabled.' + "catalog_name": + "description": |- + The name of the catalog in Unity Catalog. NOTE: On update, you cannot change the catalog name if the inference table is already enabled. + "enabled": + "description": |- + Indicates whether the inference table is enabled. + "schema_name": + "description": |- + The name of the schema in Unity Catalog. NOTE: On update, you cannot change the schema name if the inference table is already enabled. + "table_name_prefix": + "description": |- + The prefix of the table in Unity Catalog. NOTE: On update, you cannot change the prefix name if the inference table is already enabled. github.com/databricks/databricks-sdk-go/service/serving.CohereConfig: - cohere_api_base: - description: "This is an optional field to provide a customized base URL for the - Cohere API. \nIf left unspecified, the standard Cohere base URL is used.\n" - cohere_api_key: - description: 'The Databricks secret key reference for a Cohere API key. If you - prefer to paste your API key directly, see `cohere_api_key_plaintext`. You must - provide an API key using one of the following fields: `cohere_api_key` or `cohere_api_key_plaintext`.' - cohere_api_key_plaintext: - description: 'The Cohere API key provided as a plaintext string. If you prefer - to reference your key using Databricks Secrets, see `cohere_api_key`. You must - provide an API key using one of the following fields: `cohere_api_key` or `cohere_api_key_plaintext`.' + "cohere_api_base": + "description": "This is an optional field to provide a customized base URL for the Cohere API. \nIf left unspecified, the standard Cohere base URL is used.\n" + "cohere_api_key": + "description": |- + The Databricks secret key reference for a Cohere API key. If you prefer to paste your API key directly, see `cohere_api_key_plaintext`. You must provide an API key using one of the following fields: `cohere_api_key` or `cohere_api_key_plaintext`. + "cohere_api_key_plaintext": + "description": |- + The Cohere API key provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `cohere_api_key`. You must provide an API key using one of the following fields: `cohere_api_key` or `cohere_api_key_plaintext`. github.com/databricks/databricks-sdk-go/service/serving.DatabricksModelServingConfig: - databricks_api_token: - description: | + "databricks_api_token": + "description": | The Databricks secret key reference for a Databricks API token that corresponds to a user or service principal with Can Query access to the model serving endpoint pointed to by this external model. If you prefer to paste your API key directly, see `databricks_api_token_plaintext`. You must provide an API key using one of the following fields: `databricks_api_token` or `databricks_api_token_plaintext`. - databricks_api_token_plaintext: - description: | + "databricks_api_token_plaintext": + "description": | The Databricks API token that corresponds to a user or service principal with Can Query access to the model serving endpoint pointed to by this external model provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `databricks_api_token`. You must provide an API key using one of the following fields: `databricks_api_token` or `databricks_api_token_plaintext`. - databricks_workspace_url: - description: | + "databricks_workspace_url": + "description": | The URL of the Databricks workspace containing the model serving endpoint pointed to by this external model. github.com/databricks/databricks-sdk-go/service/serving.EndpointCoreConfigInput: - auto_capture_config: - description: Configuration for Inference Tables which automatically logs requests - and responses to Unity Catalog. - served_entities: - description: A list of served entities for the endpoint to serve. A serving endpoint - can have up to 15 served entities. - served_models: - description: (Deprecated, use served_entities instead) A list of served models - for the endpoint to serve. A serving endpoint can have up to 15 served models. - traffic_config: - description: The traffic config defining how invocations to the serving endpoint - should be routed. + "auto_capture_config": + "description": |- + Configuration for Inference Tables which automatically logs requests and responses to Unity Catalog. + "served_entities": + "description": |- + A list of served entities for the endpoint to serve. A serving endpoint can have up to 15 served entities. + "served_models": + "description": |- + (Deprecated, use served_entities instead) A list of served models for the endpoint to serve. A serving endpoint can have up to 15 served models. + "traffic_config": + "description": |- + The traffic config defining how invocations to the serving endpoint should be routed. github.com/databricks/databricks-sdk-go/service/serving.EndpointTag: - key: - description: Key field for a serving endpoint tag. - value: - description: Optional value field for a serving endpoint tag. + "key": + "description": |- + Key field for a serving endpoint tag. + "value": + "description": |- + Optional value field for a serving endpoint tag. github.com/databricks/databricks-sdk-go/service/serving.ExternalModel: - ai21labs_config: - description: AI21Labs Config. Only required if the provider is 'ai21labs'. - amazon_bedrock_config: - description: Amazon Bedrock Config. Only required if the provider is 'amazon-bedrock'. - anthropic_config: - description: Anthropic Config. Only required if the provider is 'anthropic'. - cohere_config: - description: Cohere Config. Only required if the provider is 'cohere'. - databricks_model_serving_config: - description: Databricks Model Serving Config. Only required if the provider is - 'databricks-model-serving'. - google_cloud_vertex_ai_config: - description: Google Cloud Vertex AI Config. Only required if the provider is 'google-cloud-vertex-ai'. - name: - description: The name of the external model. - openai_config: - description: OpenAI Config. Only required if the provider is 'openai'. - palm_config: - description: PaLM Config. Only required if the provider is 'palm'. - provider: - description: | + "ai21labs_config": + "description": |- + AI21Labs Config. Only required if the provider is 'ai21labs'. + "amazon_bedrock_config": + "description": |- + Amazon Bedrock Config. Only required if the provider is 'amazon-bedrock'. + "anthropic_config": + "description": |- + Anthropic Config. Only required if the provider is 'anthropic'. + "cohere_config": + "description": |- + Cohere Config. Only required if the provider is 'cohere'. + "databricks_model_serving_config": + "description": |- + Databricks Model Serving Config. Only required if the provider is 'databricks-model-serving'. + "google_cloud_vertex_ai_config": + "description": |- + Google Cloud Vertex AI Config. Only required if the provider is 'google-cloud-vertex-ai'. + "name": + "description": |- + The name of the external model. + "openai_config": + "description": |- + OpenAI Config. Only required if the provider is 'openai'. + "palm_config": + "description": |- + PaLM Config. Only required if the provider is 'palm'. + "provider": + "description": | The name of the provider for the external model. Currently, the supported providers are 'ai21labs', 'anthropic', 'amazon-bedrock', 'cohere', 'databricks-model-serving', 'google-cloud-vertex-ai', 'openai', and 'palm'.", - task: - description: The task type of the external model. + "task": + "description": |- + The task type of the external model. github.com/databricks/databricks-sdk-go/service/serving.ExternalModelProvider: - _: - description: | + "_": + "description": | The name of the provider for the external model. Currently, the supported providers are 'ai21labs', 'anthropic', 'amazon-bedrock', 'cohere', 'databricks-model-serving', 'google-cloud-vertex-ai', 'openai', and 'palm'.", - enum: - - ai21labs - - anthropic - - amazon-bedrock - - cohere - - databricks-model-serving - - google-cloud-vertex-ai - - openai - - palm + "enum": + - |- + ai21labs + - |- + anthropic + - |- + amazon-bedrock + - |- + cohere + - |- + databricks-model-serving + - |- + google-cloud-vertex-ai + - |- + openai + - |- + palm +github.com/databricks/databricks-sdk-go/service/serving.GoogleCloudVertexAiConfig: + "private_key": + "description": |- + The Databricks secret key reference for a private key for the service account which has access to the Google Cloud Vertex AI Service. See [Best practices for managing service account keys](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys). If you prefer to paste your API key directly, see `private_key_plaintext`. You must provide an API key using one of the following fields: `private_key` or `private_key_plaintext` + "private_key_plaintext": + "description": |- + The private key for the service account which has access to the Google Cloud Vertex AI Service provided as a plaintext secret. See [Best practices for managing service account keys](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys). If you prefer to reference your key using Databricks Secrets, see `private_key`. You must provide an API key using one of the following fields: `private_key` or `private_key_plaintext`. + "project_id": + "description": |- + This is the Google Cloud project id that the service account is associated with. + "region": + "description": |- + This is the region for the Google Cloud Vertex AI Service. See [supported regions](https://cloud.google.com/vertex-ai/docs/general/locations) for more details. Some models are only available in specific regions. +github.com/databricks/databricks-sdk-go/service/serving.OpenAiConfig: + "microsoft_entra_client_id": + "description": | + This field is only required for Azure AD OpenAI and is the Microsoft Entra Client ID. + "microsoft_entra_client_secret": + "description": | + The Databricks secret key reference for a client secret used for Microsoft Entra ID authentication. + If you prefer to paste your client secret directly, see `microsoft_entra_client_secret_plaintext`. + You must provide an API key using one of the following fields: `microsoft_entra_client_secret` or `microsoft_entra_client_secret_plaintext`. + "microsoft_entra_client_secret_plaintext": + "description": | + The client secret used for Microsoft Entra ID authentication provided as a plaintext string. + If you prefer to reference your key using Databricks Secrets, see `microsoft_entra_client_secret`. + You must provide an API key using one of the following fields: `microsoft_entra_client_secret` or `microsoft_entra_client_secret_plaintext`. + "microsoft_entra_tenant_id": + "description": | + This field is only required for Azure AD OpenAI and is the Microsoft Entra Tenant ID. + "openai_api_base": + "description": | + This is a field to provide a customized base URl for the OpenAI API. + For Azure OpenAI, this field is required, and is the base URL for the Azure OpenAI API service + provided by Azure. + For other OpenAI API types, this field is optional, and if left unspecified, the standard OpenAI base URL is used. + "openai_api_key": + "description": |- + The Databricks secret key reference for an OpenAI API key using the OpenAI or Azure service. If you prefer to paste your API key directly, see `openai_api_key_plaintext`. You must provide an API key using one of the following fields: `openai_api_key` or `openai_api_key_plaintext`. + "openai_api_key_plaintext": + "description": |- + The OpenAI API key using the OpenAI or Azure service provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `openai_api_key`. You must provide an API key using one of the following fields: `openai_api_key` or `openai_api_key_plaintext`. + "openai_api_type": + "description": | + This is an optional field to specify the type of OpenAI API to use. + For Azure OpenAI, this field is required, and adjust this parameter to represent the preferred security + access validation protocol. For access token validation, use azure. For authentication using Azure Active + Directory (Azure AD) use, azuread. + "openai_api_version": + "description": | + This is an optional field to specify the OpenAI API version. + For Azure OpenAI, this field is required, and is the version of the Azure OpenAI service to + utilize, specified by a date. + "openai_deployment_name": + "description": | + This field is only required for Azure OpenAI and is the name of the deployment resource for the + Azure OpenAI service. + "openai_organization": + "description": | + This is an optional field to specify the organization in OpenAI or Azure OpenAI. +github.com/databricks/databricks-sdk-go/service/serving.PaLmConfig: + "palm_api_key": + "description": |- + The Databricks secret key reference for a PaLM API key. If you prefer to paste your API key directly, see `palm_api_key_plaintext`. You must provide an API key using one of the following fields: `palm_api_key` or `palm_api_key_plaintext`. + "palm_api_key_plaintext": + "description": |- + The PaLM API key provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `palm_api_key`. You must provide an API key using one of the following fields: `palm_api_key` or `palm_api_key_plaintext`. github.com/databricks/databricks-sdk-go/service/serving.RateLimit: - calls: - description: Used to specify how many calls are allowed for a key within the renewal_period. - key: - description: Key field for a serving endpoint rate limit. Currently, only 'user' - and 'endpoint' are supported, with 'endpoint' being the default if not specified. - renewal_period: - description: Renewal period field for a serving endpoint rate limit. Currently, - only 'minute' is supported. + "calls": + "description": |- + Used to specify how many calls are allowed for a key within the renewal_period. + "key": + "description": |- + Key field for a serving endpoint rate limit. Currently, only 'user' and 'endpoint' are supported, with 'endpoint' being the default if not specified. + "renewal_period": + "description": |- + Renewal period field for a serving endpoint rate limit. Currently, only 'minute' is supported. github.com/databricks/databricks-sdk-go/service/serving.RateLimitKey: - _: - description: Key field for a serving endpoint rate limit. Currently, only 'user' - and 'endpoint' are supported, with 'endpoint' being the default if not specified. - enum: - - user - - endpoint + "_": + "description": |- + Key field for a serving endpoint rate limit. Currently, only 'user' and 'endpoint' are supported, with 'endpoint' being the default if not specified. + "enum": + - |- + user + - |- + endpoint github.com/databricks/databricks-sdk-go/service/serving.RateLimitRenewalPeriod: - _: - description: Renewal period field for a serving endpoint rate limit. Currently, - only 'minute' is supported. - enum: - - minute + "_": + "description": |- + Renewal period field for a serving endpoint rate limit. Currently, only 'minute' is supported. + "enum": + - |- + minute github.com/databricks/databricks-sdk-go/service/serving.Route: - served_model_name: - description: The name of the served model this route configures traffic for. - traffic_percentage: - description: The percentage of endpoint traffic to send to this route. It must - be an integer between 0 and 100 inclusive. + "served_model_name": + "description": |- + The name of the served model this route configures traffic for. + "traffic_percentage": + "description": |- + The percentage of endpoint traffic to send to this route. It must be an integer between 0 and 100 inclusive. github.com/databricks/databricks-sdk-go/service/serving.ServedEntityInput: - entity_name: - description: | + "entity_name": + "description": | The name of the entity to be served. The entity may be a model in the Databricks Model Registry, a model in the Unity Catalog (UC), or a function of type FEATURE_SPEC in the UC. If it is a UC object, the full name of the object should be given in the form of __catalog_name__.__schema_name__.__model_name__. - entity_version: - description: The version of the model in Databricks Model Registry to be served - or empty if the entity is a FEATURE_SPEC. - environment_vars: - description: "An object containing a set of optional, user-specified environment - variable key-value pairs used for serving this entity.\nNote: this is an experimental - feature and subject to change. \nExample entity environment variables that refer - to Databricks secrets: `{\"OPENAI_API_KEY\": \"{{secrets/my_scope/my_key}}\", - \"DATABRICKS_TOKEN\": \"{{secrets/my_scope2/my_key2}}\"}`" - external_model: - description: | + "entity_version": + "description": |- + The version of the model in Databricks Model Registry to be served or empty if the entity is a FEATURE_SPEC. + "environment_vars": + "description": "An object containing a set of optional, user-specified environment variable key-value pairs used for serving this entity.\nNote: this is an experimental feature and subject to change. \nExample entity environment variables that refer to Databricks secrets: `{\"OPENAI_API_KEY\": \"{{secrets/my_scope/my_key}}\", \"DATABRICKS_TOKEN\": \"{{secrets/my_scope2/my_key2}}\"}`" + "external_model": + "description": | The external model to be served. NOTE: Only one of external_model and (entity_name, entity_version, workload_size, workload_type, and scale_to_zero_enabled) can be specified with the latter set being used for custom model serving for a Databricks registered model. For an existing endpoint with external_model, it cannot be updated to an endpoint without external_model. If the endpoint is created without external_model, users cannot update it to add external_model later. The task type of all external models within an endpoint must be the same. - instance_profile_arn: - description: ARN of the instance profile that the served entity uses to access - AWS resources. - max_provisioned_throughput: - description: The maximum tokens per second that the endpoint can scale up to. - min_provisioned_throughput: - description: The minimum tokens per second that the endpoint can scale down to. - name: - description: | + "instance_profile_arn": + "description": |- + ARN of the instance profile that the served entity uses to access AWS resources. + "max_provisioned_throughput": + "description": |- + The maximum tokens per second that the endpoint can scale up to. + "min_provisioned_throughput": + "description": |- + The minimum tokens per second that the endpoint can scale down to. + "name": + "description": | The name of a served entity. It must be unique across an endpoint. A served entity name can consist of alphanumeric characters, dashes, and underscores. If not specified for an external model, this field defaults to external_model.name, with '.' and ':' replaced with '-', and if not specified for other entities, it defaults to -. - scale_to_zero_enabled: - description: Whether the compute resources for the served entity should scale - down to zero. - workload_size: - description: | + "scale_to_zero_enabled": + "description": |- + Whether the compute resources for the served entity should scale down to zero. + "workload_size": + "description": | The workload size of the served entity. The workload size corresponds to a range of provisioned concurrency that the compute autoscales between. A single unit of provisioned concurrency can process one request at a time. Valid workload sizes are "Small" (4 - 4 provisioned concurrency), "Medium" (8 - 16 provisioned concurrency), and "Large" (16 - 64 provisioned concurrency). If scale-to-zero is enabled, the lower bound of the provisioned concurrency for each workload size is 0. - workload_type: - description: | + "workload_type": + "description": | The workload type of the served entity. The workload type selects which type of compute to use in the endpoint. The default value for this parameter is "CPU". For deep learning workloads, GPU acceleration is available by selecting workload types like GPU_SMALL and others. See the available [GPU types](https://docs.databricks.com/machine-learning/model-serving/create-manage-serving-endpoints.html#gpu-workload-types). github.com/databricks/databricks-sdk-go/service/serving.ServedModelInput: - environment_vars: - description: "An object containing a set of optional, user-specified environment - variable key-value pairs used for serving this model.\nNote: this is an experimental - feature and subject to change. \nExample model environment variables that refer - to Databricks secrets: `{\"OPENAI_API_KEY\": \"{{secrets/my_scope/my_key}}\", - \"DATABRICKS_TOKEN\": \"{{secrets/my_scope2/my_key2}}\"}`" - instance_profile_arn: - description: ARN of the instance profile that the served model will use to access - AWS resources. - max_provisioned_throughput: - description: The maximum tokens per second that the endpoint can scale up to. - min_provisioned_throughput: - description: The minimum tokens per second that the endpoint can scale down to. - model_name: - description: | + "environment_vars": + "description": "An object containing a set of optional, user-specified environment variable key-value pairs used for serving this model.\nNote: this is an experimental feature and subject to change. \nExample model environment variables that refer to Databricks secrets: `{\"OPENAI_API_KEY\": \"{{secrets/my_scope/my_key}}\", \"DATABRICKS_TOKEN\": \"{{secrets/my_scope2/my_key2}}\"}`" + "instance_profile_arn": + "description": |- + ARN of the instance profile that the served model will use to access AWS resources. + "max_provisioned_throughput": + "description": |- + The maximum tokens per second that the endpoint can scale up to. + "min_provisioned_throughput": + "description": |- + The minimum tokens per second that the endpoint can scale down to. + "model_name": + "description": | The name of the model in Databricks Model Registry to be served or if the model resides in Unity Catalog, the full name of model, in the form of __catalog_name__.__schema_name__.__model_name__. - model_version: - description: The version of the model in Databricks Model Registry or Unity Catalog - to be served. - name: - description: | + "model_version": + "description": |- + The version of the model in Databricks Model Registry or Unity Catalog to be served. + "name": + "description": | The name of a served model. It must be unique across an endpoint. If not specified, this field will default to -. A served model name can consist of alphanumeric characters, dashes, and underscores. - scale_to_zero_enabled: - description: Whether the compute resources for the served model should scale down - to zero. - workload_size: - description: | + "scale_to_zero_enabled": + "description": |- + Whether the compute resources for the served model should scale down to zero. + "workload_size": + "description": | The workload size of the served model. The workload size corresponds to a range of provisioned concurrency that the compute will autoscale between. A single unit of provisioned concurrency can process one request at a time. Valid workload sizes are "Small" (4 - 4 provisioned concurrency), "Medium" (8 - 16 provisioned concurrency), and "Large" (16 - 64 provisioned concurrency). If scale-to-zero is enabled, the lower bound of the provisioned concurrency for each workload size will be 0. - workload_type: - description: | + "workload_type": + "description": | The workload type of the served model. The workload type selects which type of compute to use in the endpoint. The default value for this parameter is "CPU". For deep learning workloads, GPU acceleration is available by selecting workload types like GPU_SMALL and others. See the available [GPU types](https://docs.databricks.com/machine-learning/model-serving/create-manage-serving-endpoints.html#gpu-workload-types). github.com/databricks/databricks-sdk-go/service/serving.ServedModelInputWorkloadSize: - _: - description: | + "_": + "description": | The workload size of the served model. The workload size corresponds to a range of provisioned concurrency that the compute will autoscale between. A single unit of provisioned concurrency can process one request at a time. Valid workload sizes are "Small" (4 - 4 provisioned concurrency), "Medium" (8 - 16 provisioned concurrency), and "Large" (16 - 64 provisioned concurrency). If scale-to-zero is enabled, the lower bound of the provisioned concurrency for each workload size will be 0. - enum: - - Small - - Medium - - Large + "enum": + - |- + Small + - |- + Medium + - |- + Large github.com/databricks/databricks-sdk-go/service/serving.ServedModelInputWorkloadType: - _: - description: | + "_": + "description": | The workload type of the served model. The workload type selects which type of compute to use in the endpoint. The default value for this parameter is "CPU". For deep learning workloads, GPU acceleration is available by selecting workload types like GPU_SMALL and others. See the available [GPU types](https://docs.databricks.com/machine-learning/model-serving/create-manage-serving-endpoints.html#gpu-workload-types). - enum: - - CPU - - GPU_SMALL - - GPU_MEDIUM - - GPU_LARGE - - MULTIGPU_MEDIUM + "enum": + - |- + CPU + - |- + GPU_SMALL + - |- + GPU_MEDIUM + - |- + GPU_LARGE + - |- + MULTIGPU_MEDIUM github.com/databricks/databricks-sdk-go/service/serving.TrafficConfig: - routes: - description: The list of routes that define traffic to each served entity. + "routes": + "description": |- + The list of routes that define traffic to each served entity. diff --git a/bundle/internal/schema/main_test.go b/bundle/internal/schema/main_test.go index 607347b6b..4eeb41d47 100644 --- a/bundle/internal/schema/main_test.go +++ b/bundle/internal/schema/main_test.go @@ -42,7 +42,8 @@ func copyFile(src, dst string) error { // Checks whether descriptions are added for new config fields in the annotations.yml file // If this test fails either manually add descriptions to the `annotations.yml` or do the following: -// 1. run `make schema` from the repository root to add placeholder descriptions +// 1. for fields described outside of CLI package fetch latest schema from the OpenAPI spec and add path to file to DATABRICKS_OPENAPI_SPEC env variable +// 2. run `make schema` from the repository root to add placeholder descriptions // 2. replace all "PLACEHOLDER" values with the actual descriptions if possible // 3. run `make schema` again to regenerate the schema with acutal descriptions func TestRequiredAnnotationsForNewFields(t *testing.T) { diff --git a/bundle/internal/schema/parser.go b/bundle/internal/schema/parser.go index 3fbec0528..e1d1a13dc 100644 --- a/bundle/internal/schema/parser.go +++ b/bundle/internal/schema/parser.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "encoding/json" "fmt" "os" @@ -9,7 +8,6 @@ import ( "reflect" "strings" - "github.com/databricks/cli/libs/dyn/yamlloader" "github.com/databricks/cli/libs/jsonschema" "gopkg.in/yaml.v3" ) @@ -83,7 +81,11 @@ func (p *openapiParser) findRef(typ reflect.Type) (jsonschema.Schema, bool) { // Skip if the type is not in the openapi spec. _, ok := p.ref[k] if !ok { - continue + k = mapIncorrectTypNames(k) + _, ok = p.ref[k] + if !ok { + continue + } } // Return the first Go SDK type found in the openapi spec. @@ -93,6 +95,23 @@ func (p *openapiParser) findRef(typ reflect.Type) (jsonschema.Schema, bool) { return jsonschema.Schema{}, false } +// Fix inconsistent type names between the Go SDK and the OpenAPI spec. +// E.g. "serving.PaLmConfig" in the Go SDK is "serving.PaLMConfig" in the OpenAPI spec. +func mapIncorrectTypNames(ref string) string { + switch ref { + case "serving.PaLmConfig": + return "serving.PaLMConfig" + case "serving.OpenAiConfig": + return "serving.OpenAIConfig" + case "serving.GoogleCloudVertexAiConfig": + return "serving.GoogleCloudVertexAIConfig" + case "serving.Ai21LabsConfig": + return "serving.AI21LabsConfig" + default: + return ref + } +} + // Use the OpenAPI spec to load descriptions for the given type. func (p *openapiParser) extractAnnotations(typ reflect.Type, outputPath, overridesPath string) error { annotations := annotationFile{} @@ -142,31 +161,40 @@ func (p *openapiParser) extractAnnotations(typ reflect.Type, outputPath, overrid return err } - b, err = yaml.Marshal(overrides) + err = saveYamlWithStyle(overridesPath, overrides) if err != nil { return err } - o, err := yamlloader.LoadYAML("", bytes.NewBuffer(b)) + err = saveYamlWithStyle(outputPath, annotations) if err != nil { return err } - err = saveYamlWithStyle(overridesPath, o) + err = prependCommentToFile(outputPath, "# This file is auto-generated. DO NOT EDIT.\n") if err != nil { return err } - b, err = yaml.Marshal(annotations) - if err != nil { - return err - } - b = bytes.Join([][]byte{[]byte("# This file is auto-generated. DO NOT EDIT."), b}, []byte("\n")) - err = os.WriteFile(outputPath, b, 0o644) - if err != nil { - return err - } - return nil } +func prependCommentToFile(outputPath, comment string) error { + b, err := os.ReadFile(outputPath) + if err != nil { + return err + } + f, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(comment) + if err != nil { + return err + } + _, err = f.Write(b) + return err +} + func addEmptyOverride(key, pkg string, overridesFile annotationFile) { if overridesFile[pkg] == nil { overridesFile[pkg] = map[string]annotation{} diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 8e8efa7fc..9a352ebb2 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -4772,9 +4772,11 @@ "type": "object", "properties": { "ai21labs_api_key": { + "description": "The Databricks secret key reference for an AI21 Labs API key. If you prefer to paste your API key directly, see `ai21labs_api_key_plaintext`. You must provide an API key using one of the following fields: `ai21labs_api_key` or `ai21labs_api_key_plaintext`.", "$ref": "#/$defs/string" }, "ai21labs_api_key_plaintext": { + "description": "An AI21 Labs API key provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `ai21labs_api_key`. You must provide an API key using one of the following fields: `ai21labs_api_key` or `ai21labs_api_key_plaintext`.", "$ref": "#/$defs/string" } }, @@ -5287,15 +5289,19 @@ "type": "object", "properties": { "private_key": { + "description": "The Databricks secret key reference for a private key for the service account which has access to the Google Cloud Vertex AI Service. See [Best practices for managing service account keys](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys). If you prefer to paste your API key directly, see `private_key_plaintext`. You must provide an API key using one of the following fields: `private_key` or `private_key_plaintext`", "$ref": "#/$defs/string" }, "private_key_plaintext": { + "description": "The private key for the service account which has access to the Google Cloud Vertex AI Service provided as a plaintext secret. See [Best practices for managing service account keys](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys). If you prefer to reference your key using Databricks Secrets, see `private_key`. You must provide an API key using one of the following fields: `private_key` or `private_key_plaintext`.", "$ref": "#/$defs/string" }, "project_id": { + "description": "This is the Google Cloud project id that the service account is associated with.", "$ref": "#/$defs/string" }, "region": { + "description": "This is the region for the Google Cloud Vertex AI Service. See [supported regions](https://cloud.google.com/vertex-ai/docs/general/locations) for more details. Some models are only available in specific regions.", "$ref": "#/$defs/string" } }, @@ -5313,36 +5319,47 @@ "type": "object", "properties": { "microsoft_entra_client_id": { + "description": "This field is only required for Azure AD OpenAI and is the Microsoft Entra Client ID.\n", "$ref": "#/$defs/string" }, "microsoft_entra_client_secret": { + "description": "The Databricks secret key reference for a client secret used for Microsoft Entra ID authentication.\nIf you prefer to paste your client secret directly, see `microsoft_entra_client_secret_plaintext`.\nYou must provide an API key using one of the following fields: `microsoft_entra_client_secret` or `microsoft_entra_client_secret_plaintext`.\n", "$ref": "#/$defs/string" }, "microsoft_entra_client_secret_plaintext": { + "description": "The client secret used for Microsoft Entra ID authentication provided as a plaintext string.\nIf you prefer to reference your key using Databricks Secrets, see `microsoft_entra_client_secret`.\nYou must provide an API key using one of the following fields: `microsoft_entra_client_secret` or `microsoft_entra_client_secret_plaintext`.\n", "$ref": "#/$defs/string" }, "microsoft_entra_tenant_id": { + "description": "This field is only required for Azure AD OpenAI and is the Microsoft Entra Tenant ID.\n", "$ref": "#/$defs/string" }, "openai_api_base": { + "description": "This is a field to provide a customized base URl for the OpenAI API.\nFor Azure OpenAI, this field is required, and is the base URL for the Azure OpenAI API service\nprovided by Azure.\nFor other OpenAI API types, this field is optional, and if left unspecified, the standard OpenAI base URL is used.\n", "$ref": "#/$defs/string" }, "openai_api_key": { + "description": "The Databricks secret key reference for an OpenAI API key using the OpenAI or Azure service. If you prefer to paste your API key directly, see `openai_api_key_plaintext`. You must provide an API key using one of the following fields: `openai_api_key` or `openai_api_key_plaintext`.", "$ref": "#/$defs/string" }, "openai_api_key_plaintext": { + "description": "The OpenAI API key using the OpenAI or Azure service provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `openai_api_key`. You must provide an API key using one of the following fields: `openai_api_key` or `openai_api_key_plaintext`.", "$ref": "#/$defs/string" }, "openai_api_type": { + "description": "This is an optional field to specify the type of OpenAI API to use.\nFor Azure OpenAI, this field is required, and adjust this parameter to represent the preferred security\naccess validation protocol. For access token validation, use azure. For authentication using Azure Active\nDirectory (Azure AD) use, azuread.\n", "$ref": "#/$defs/string" }, "openai_api_version": { + "description": "This is an optional field to specify the OpenAI API version.\nFor Azure OpenAI, this field is required, and is the version of the Azure OpenAI service to\nutilize, specified by a date.\n", "$ref": "#/$defs/string" }, "openai_deployment_name": { + "description": "This field is only required for Azure OpenAI and is the name of the deployment resource for the\nAzure OpenAI service.\n", "$ref": "#/$defs/string" }, "openai_organization": { + "description": "This is an optional field to specify the organization in OpenAI or Azure OpenAI.\n", "$ref": "#/$defs/string" } }, @@ -5360,9 +5377,11 @@ "type": "object", "properties": { "palm_api_key": { + "description": "The Databricks secret key reference for a PaLM API key. If you prefer to paste your API key directly, see `palm_api_key_plaintext`. You must provide an API key using one of the following fields: `palm_api_key` or `palm_api_key_plaintext`.", "$ref": "#/$defs/string" }, "palm_api_key_plaintext": { + "description": "The PaLM API key provided as a plaintext string. If you prefer to reference your key using Databricks Secrets, see `palm_api_key`. You must provide an API key using one of the following fields: `palm_api_key` or `palm_api_key_plaintext`.", "$ref": "#/$defs/string" } }, From a002475a6a4175cb0fa4ef09e21ef8bcecd84899 Mon Sep 17 00:00:00 2001 From: "Lennart Kats (databricks)" Date: Fri, 27 Dec 2024 12:38:12 +0100 Subject: [PATCH 02/53] Relax checks in builtin template tests (#2042) ## Changes Relax the checks of `lib/template/builtin_test` so they don't fail for a local development copy that has uncommitted draft templates. Right now these tests fail because I have some git-ignored uncommitted templates in my local dev copy. --- libs/template/builtin_test.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/libs/template/builtin_test.go b/libs/template/builtin_test.go index 504e0acca..79e04cb84 100644 --- a/libs/template/builtin_test.go +++ b/libs/template/builtin_test.go @@ -11,18 +11,24 @@ import ( func TestBuiltin(t *testing.T) { out, err := Builtin() require.NoError(t, err) - assert.Len(t, out, 3) + assert.GreaterOrEqual(t, len(out), 3) - // Confirm names. - assert.Equal(t, "dbt-sql", out[0].Name) - assert.Equal(t, "default-python", out[1].Name) - assert.Equal(t, "default-sql", out[2].Name) + // Create a map of templates by name for easier lookup + templates := make(map[string]*BuiltinTemplate) + for _, tmpl := range out { + templates[tmpl.Name] = &tmpl + } - // Confirm that the filesystems work. - _, err = fs.Stat(out[0].FS, `template/{{.project_name}}/dbt_project.yml.tmpl`) + // Verify all expected templates exist + assert.Contains(t, templates, "dbt-sql") + assert.Contains(t, templates, "default-python") + assert.Contains(t, templates, "default-sql") + + // Verify the filesystems work for each template + _, err = fs.Stat(templates["dbt-sql"].FS, `template/{{.project_name}}/dbt_project.yml.tmpl`) assert.NoError(t, err) - _, err = fs.Stat(out[1].FS, `template/{{.project_name}}/tests/main_test.py.tmpl`) + _, err = fs.Stat(templates["default-python"].FS, `template/{{.project_name}}/tests/main_test.py.tmpl`) assert.NoError(t, err) - _, err = fs.Stat(out[2].FS, `template/{{.project_name}}/src/orders_daily.sql.tmpl`) + _, err = fs.Stat(templates["default-sql"].FS, `template/{{.project_name}}/src/orders_daily.sql.tmpl`) assert.NoError(t, err) } From e088d0d996979ddd56dd9ce10d3d1becbdc771f6 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 30 Dec 2024 16:18:57 +0100 Subject: [PATCH 03/53] Add lint.sh to run golanci-lint in 2 stages (#2051) First stage is to run goimports and formatter, second is full suite. This ensures that imports and formatting are fixed even in presence of other issues. Otherwise golanci-lint refuses to fix anything https://github.com/golangci/golangci-lint/issues/5257 This helpful when running aider with config like this - aider will use that to autofix what it can after every update: ``` % cat .aider.conf.yml lint-cmd: - "go: ./lint.sh" ``` --- Makefile | 2 +- lint.sh | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100755 lint.sh diff --git a/Makefile b/Makefile index f8e7834a5..7dca3b2cf 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ default: build lint: vendor @echo "✓ Linting source code with https://golangci-lint.run/ (with --fix)..." - @golangci-lint run --fix ./... + @./lint.sh ./... lintcheck: vendor @echo "✓ Linting source code with https://golangci-lint.run/ ..." diff --git a/lint.sh b/lint.sh new file mode 100755 index 000000000..c93d04c66 --- /dev/null +++ b/lint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail +# With golangci-lint, if there are any compliation issues, then formatters' autofix won't be applied. +# https://github.com/golangci/golangci-lint/issues/5257 +# However, running goimports first alone will actually fix some of the compilation issues. +# Fixing formatting is also reasonable thing to do. +# For this reason, this script runs golangci-lint in two stages: +golangci-lint run --fix --no-config --disable-all --enable gofumpt,goimports $@ +exec golangci-lint run --fix $@ From 261b7f4083c4f10c0cc0e6c0ec9d95bfc2927174 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 30 Dec 2024 16:26:21 +0100 Subject: [PATCH 04/53] Move bulk of "golden tests" logic to libs/testdiff (#2054) ## Changes - Detach "golden files" assertions from testcli runner. Now any output can be compared, no matter how it is obtained. - Move those assertion to libs/testdiff package. This allows using "golden files" in non-integration tests. ## Tests Existing tests --- .../bundle/init_default_python_test.go | 7 +- internal/testcli/golden.go | 201 +---------------- libs/testdiff/golden.go | 212 ++++++++++++++++++ .../testcli => libs/testdiff}/golden_test.go | 2 +- 4 files changed, 220 insertions(+), 202 deletions(-) create mode 100644 libs/testdiff/golden.go rename {internal/testcli => libs/testdiff}/golden_test.go (92%) diff --git a/integration/bundle/init_default_python_test.go b/integration/bundle/init_default_python_test.go index 9b65636e9..c93e6b50b 100644 --- a/integration/bundle/init_default_python_test.go +++ b/integration/bundle/init_default_python_test.go @@ -11,6 +11,7 @@ import ( "github.com/databricks/cli/internal/testcli" "github.com/databricks/cli/internal/testutil" "github.com/databricks/cli/libs/python/pythontest" + "github.com/databricks/cli/libs/testdiff" "github.com/stretchr/testify/require" ) @@ -50,14 +51,14 @@ func testDefaultPython(t *testing.T, pythonVersion string) { ctx, wt := acc.WorkspaceTest(t) uniqueProjectId := testutil.RandomName("") - ctx, replacements := testcli.WithReplacementsMap(ctx) + ctx, replacements := testdiff.WithReplacementsMap(ctx) replacements.Set(uniqueProjectId, "$UNIQUE_PRJ") user, err := wt.W.CurrentUser.Me(ctx) require.NoError(t, err) require.NotNil(t, user) - testcli.PrepareReplacementsUser(t, replacements, *user) - testcli.PrepareReplacements(t, replacements, wt.W) + testdiff.PrepareReplacementsUser(t, replacements, *user) + testdiff.PrepareReplacements(t, replacements, wt.W) tmpDir := t.TempDir() testutil.Chdir(t, tmpDir) diff --git a/internal/testcli/golden.go b/internal/testcli/golden.go index 34f38f18a..a16f42c72 100644 --- a/internal/testcli/golden.go +++ b/internal/testcli/golden.go @@ -3,222 +3,27 @@ package testcli import ( "context" "fmt" - "os" - "regexp" - "slices" "strings" - "testing" "github.com/databricks/cli/internal/testutil" - "github.com/databricks/cli/libs/iamutil" "github.com/databricks/cli/libs/testdiff" - "github.com/databricks/databricks-sdk-go" - "github.com/databricks/databricks-sdk-go/service/iam" "github.com/stretchr/testify/assert" ) -var OverwriteMode = os.Getenv("TESTS_OUTPUT") == "OVERWRITE" - -func ReadFile(t testutil.TestingT, ctx context.Context, filename string) string { - data, err := os.ReadFile(filename) - if os.IsNotExist(err) { - return "" - } - assert.NoError(t, err) - // On CI, on Windows \n in the file somehow end up as \r\n - return NormalizeNewlines(string(data)) -} - func captureOutput(t testutil.TestingT, ctx context.Context, args []string) string { t.Logf("run args: [%s]", strings.Join(args, ", ")) r := NewRunner(t, ctx, args...) stdout, stderr, err := r.Run() assert.NoError(t, err) - out := stderr.String() + stdout.String() - return ReplaceOutput(t, ctx, out) -} - -func WriteFile(t testutil.TestingT, filename, data string) { - t.Logf("Overwriting %s", filename) - err := os.WriteFile(filename, []byte(data), 0o644) - assert.NoError(t, err) + return stderr.String() + stdout.String() } func AssertOutput(t testutil.TestingT, ctx context.Context, args []string, expectedPath string) { - expected := ReadFile(t, ctx, expectedPath) - out := captureOutput(t, ctx, args) - - if out != expected { - actual := fmt.Sprintf("Output from %v", args) - testdiff.AssertEqualTexts(t, expectedPath, actual, expected, out) - - if OverwriteMode { - WriteFile(t, expectedPath, out) - } - } + testdiff.AssertOutput(t, ctx, out, fmt.Sprintf("Output from %v", args), expectedPath) } func AssertOutputJQ(t testutil.TestingT, ctx context.Context, args []string, expectedPath string, ignorePaths []string) { - expected := ReadFile(t, ctx, expectedPath) - out := captureOutput(t, ctx, args) - - if out != expected { - actual := fmt.Sprintf("Output from %v", args) - testdiff.AssertEqualJQ(t.(*testing.T), expectedPath, actual, expected, out, ignorePaths) - - if OverwriteMode { - WriteFile(t, expectedPath, out) - } - } -} - -var ( - uuidRegex = regexp.MustCompile(`[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}`) - numIdRegex = regexp.MustCompile(`[0-9]{3,}`) - privatePathRegex = regexp.MustCompile(`(/tmp|/private)(/.*)/([a-zA-Z0-9]+)`) -) - -func ReplaceOutput(t testutil.TestingT, ctx context.Context, out string) string { - out = NormalizeNewlines(out) - replacements := GetReplacementsMap(ctx) - if replacements == nil { - t.Fatal("WithReplacementsMap was not called") - } - out = replacements.Replace(out) - out = uuidRegex.ReplaceAllString(out, "") - out = numIdRegex.ReplaceAllString(out, "") - out = privatePathRegex.ReplaceAllString(out, "/tmp/.../$3") - - return out -} - -type key int - -const ( - replacementsMapKey = key(1) -) - -type Replacement struct { - Old string - New string -} - -type ReplacementsContext struct { - Repls []Replacement -} - -func (r *ReplacementsContext) Replace(s string) string { - // QQQ Should probably only replace whole words - for _, repl := range r.Repls { - s = strings.ReplaceAll(s, repl.Old, repl.New) - } - return s -} - -func (r *ReplacementsContext) Set(old, new string) { - if old == "" || new == "" { - return - } - r.Repls = append(r.Repls, Replacement{Old: old, New: new}) -} - -func WithReplacementsMap(ctx context.Context) (context.Context, *ReplacementsContext) { - value := ctx.Value(replacementsMapKey) - if value != nil { - if existingMap, ok := value.(*ReplacementsContext); ok { - return ctx, existingMap - } - } - - newMap := &ReplacementsContext{} - ctx = context.WithValue(ctx, replacementsMapKey, newMap) - return ctx, newMap -} - -func GetReplacementsMap(ctx context.Context) *ReplacementsContext { - value := ctx.Value(replacementsMapKey) - if value != nil { - if existingMap, ok := value.(*ReplacementsContext); ok { - return existingMap - } - } - return nil -} - -func PrepareReplacements(t testutil.TestingT, r *ReplacementsContext, w *databricks.WorkspaceClient) { - // in some clouds (gcp) w.Config.Host includes "https://" prefix in others it's really just a host (azure) - host := strings.TrimPrefix(strings.TrimPrefix(w.Config.Host, "http://"), "https://") - r.Set(host, "$DATABRICKS_HOST") - r.Set(w.Config.ClusterID, "$DATABRICKS_CLUSTER_ID") - r.Set(w.Config.WarehouseID, "$DATABRICKS_WAREHOUSE_ID") - r.Set(w.Config.ServerlessComputeID, "$DATABRICKS_SERVERLESS_COMPUTE_ID") - r.Set(w.Config.MetadataServiceURL, "$DATABRICKS_METADATA_SERVICE_URL") - r.Set(w.Config.AccountID, "$DATABRICKS_ACCOUNT_ID") - r.Set(w.Config.Token, "$DATABRICKS_TOKEN") - r.Set(w.Config.Username, "$DATABRICKS_USERNAME") - r.Set(w.Config.Password, "$DATABRICKS_PASSWORD") - r.Set(w.Config.Profile, "$DATABRICKS_CONFIG_PROFILE") - r.Set(w.Config.ConfigFile, "$DATABRICKS_CONFIG_FILE") - r.Set(w.Config.GoogleServiceAccount, "$DATABRICKS_GOOGLE_SERVICE_ACCOUNT") - r.Set(w.Config.GoogleCredentials, "$GOOGLE_CREDENTIALS") - r.Set(w.Config.AzureResourceID, "$DATABRICKS_AZURE_RESOURCE_ID") - r.Set(w.Config.AzureClientSecret, "$ARM_CLIENT_SECRET") - // r.Set(w.Config.AzureClientID, "$ARM_CLIENT_ID") - r.Set(w.Config.AzureClientID, "$USERNAME") - r.Set(w.Config.AzureTenantID, "$ARM_TENANT_ID") - r.Set(w.Config.ActionsIDTokenRequestURL, "$ACTIONS_ID_TOKEN_REQUEST_URL") - r.Set(w.Config.ActionsIDTokenRequestToken, "$ACTIONS_ID_TOKEN_REQUEST_TOKEN") - r.Set(w.Config.AzureEnvironment, "$ARM_ENVIRONMENT") - r.Set(w.Config.ClientID, "$DATABRICKS_CLIENT_ID") - r.Set(w.Config.ClientSecret, "$DATABRICKS_CLIENT_SECRET") - r.Set(w.Config.DatabricksCliPath, "$DATABRICKS_CLI_PATH") - // This is set to words like "path" that happen too frequently - // r.Set(w.Config.AuthType, "$DATABRICKS_AUTH_TYPE") -} - -func PrepareReplacementsUser(t testutil.TestingT, r *ReplacementsContext, u iam.User) { - // There could be exact matches or overlap between different name fields, so sort them by length - // to ensure we match the largest one first and map them all to the same token - names := []string{ - u.DisplayName, - u.UserName, - iamutil.GetShortUserName(&u), - u.Name.FamilyName, - u.Name.GivenName, - } - if u.Name != nil { - names = append(names, u.Name.FamilyName) - names = append(names, u.Name.GivenName) - } - for _, val := range u.Emails { - names = append(names, val.Value) - } - stableSortReverseLength(names) - - for _, name := range names { - r.Set(name, "$USERNAME") - } - - for ind, val := range u.Groups { - r.Set(val.Value, fmt.Sprintf("$USER.Groups[%d]", ind)) - } - - r.Set(u.Id, "$USER.Id") - - for ind, val := range u.Roles { - r.Set(val.Value, fmt.Sprintf("$USER.Roles[%d]", ind)) - } -} - -func stableSortReverseLength(strs []string) { - slices.SortStableFunc(strs, func(a, b string) int { - return len(b) - len(a) - }) -} - -func NormalizeNewlines(input string) string { - output := strings.ReplaceAll(input, "\r\n", "\n") - return strings.ReplaceAll(output, "\r", "\n") + testdiff.AssertOutputJQ(t, ctx, out, fmt.Sprintf("Output from %v", args), expectedPath, ignorePaths) } diff --git a/libs/testdiff/golden.go b/libs/testdiff/golden.go new file mode 100644 index 000000000..c338ac9e5 --- /dev/null +++ b/libs/testdiff/golden.go @@ -0,0 +1,212 @@ +package testdiff + +import ( + "context" + "fmt" + "os" + "regexp" + "slices" + "strings" + "testing" + + "github.com/databricks/cli/internal/testutil" + "github.com/databricks/cli/libs/iamutil" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/iam" + "github.com/stretchr/testify/assert" +) + +var OverwriteMode = os.Getenv("TESTS_OUTPUT") == "OVERWRITE" + +func ReadFile(t testutil.TestingT, ctx context.Context, filename string) string { + data, err := os.ReadFile(filename) + if os.IsNotExist(err) { + return "" + } + assert.NoError(t, err) + // On CI, on Windows \n in the file somehow end up as \r\n + return NormalizeNewlines(string(data)) +} + +func WriteFile(t testutil.TestingT, filename, data string) { + t.Logf("Overwriting %s", filename) + err := os.WriteFile(filename, []byte(data), 0o644) + assert.NoError(t, err) +} + +func AssertOutput(t testutil.TestingT, ctx context.Context, out, outTitle, expectedPath string) { + expected := ReadFile(t, ctx, expectedPath) + + out = ReplaceOutput(t, ctx, out) + + if out != expected { + AssertEqualTexts(t, expectedPath, outTitle, expected, out) + + if OverwriteMode { + WriteFile(t, expectedPath, out) + } + } +} + +func AssertOutputJQ(t testutil.TestingT, ctx context.Context, out, outTitle, expectedPath string, ignorePaths []string) { + expected := ReadFile(t, ctx, expectedPath) + + out = ReplaceOutput(t, ctx, out) + + if out != expected { + AssertEqualJQ(t.(*testing.T), expectedPath, outTitle, expected, out, ignorePaths) + + if OverwriteMode { + WriteFile(t, expectedPath, out) + } + } +} + +var ( + uuidRegex = regexp.MustCompile(`[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}`) + numIdRegex = regexp.MustCompile(`[0-9]{3,}`) + privatePathRegex = regexp.MustCompile(`(/tmp|/private)(/.*)/([a-zA-Z0-9]+)`) +) + +func ReplaceOutput(t testutil.TestingT, ctx context.Context, out string) string { + out = NormalizeNewlines(out) + replacements := GetReplacementsMap(ctx) + if replacements == nil { + t.Fatal("WithReplacementsMap was not called") + } + out = replacements.Replace(out) + out = uuidRegex.ReplaceAllString(out, "") + out = numIdRegex.ReplaceAllString(out, "") + out = privatePathRegex.ReplaceAllString(out, "/tmp/.../$3") + + return out +} + +type key int + +const ( + replacementsMapKey = key(1) +) + +type Replacement struct { + Old string + New string +} + +type ReplacementsContext struct { + Repls []Replacement +} + +func (r *ReplacementsContext) Replace(s string) string { + // QQQ Should probably only replace whole words + for _, repl := range r.Repls { + s = strings.ReplaceAll(s, repl.Old, repl.New) + } + return s +} + +func (r *ReplacementsContext) Set(old, new string) { + if old == "" || new == "" { + return + } + r.Repls = append(r.Repls, Replacement{Old: old, New: new}) +} + +func WithReplacementsMap(ctx context.Context) (context.Context, *ReplacementsContext) { + value := ctx.Value(replacementsMapKey) + if value != nil { + if existingMap, ok := value.(*ReplacementsContext); ok { + return ctx, existingMap + } + } + + newMap := &ReplacementsContext{} + ctx = context.WithValue(ctx, replacementsMapKey, newMap) + return ctx, newMap +} + +func GetReplacementsMap(ctx context.Context) *ReplacementsContext { + value := ctx.Value(replacementsMapKey) + if value != nil { + if existingMap, ok := value.(*ReplacementsContext); ok { + return existingMap + } + } + return nil +} + +func PrepareReplacements(t testutil.TestingT, r *ReplacementsContext, w *databricks.WorkspaceClient) { + // in some clouds (gcp) w.Config.Host includes "https://" prefix in others it's really just a host (azure) + host := strings.TrimPrefix(strings.TrimPrefix(w.Config.Host, "http://"), "https://") + r.Set(host, "$DATABRICKS_HOST") + r.Set(w.Config.ClusterID, "$DATABRICKS_CLUSTER_ID") + r.Set(w.Config.WarehouseID, "$DATABRICKS_WAREHOUSE_ID") + r.Set(w.Config.ServerlessComputeID, "$DATABRICKS_SERVERLESS_COMPUTE_ID") + r.Set(w.Config.MetadataServiceURL, "$DATABRICKS_METADATA_SERVICE_URL") + r.Set(w.Config.AccountID, "$DATABRICKS_ACCOUNT_ID") + r.Set(w.Config.Token, "$DATABRICKS_TOKEN") + r.Set(w.Config.Username, "$DATABRICKS_USERNAME") + r.Set(w.Config.Password, "$DATABRICKS_PASSWORD") + r.Set(w.Config.Profile, "$DATABRICKS_CONFIG_PROFILE") + r.Set(w.Config.ConfigFile, "$DATABRICKS_CONFIG_FILE") + r.Set(w.Config.GoogleServiceAccount, "$DATABRICKS_GOOGLE_SERVICE_ACCOUNT") + r.Set(w.Config.GoogleCredentials, "$GOOGLE_CREDENTIALS") + r.Set(w.Config.AzureResourceID, "$DATABRICKS_AZURE_RESOURCE_ID") + r.Set(w.Config.AzureClientSecret, "$ARM_CLIENT_SECRET") + // r.Set(w.Config.AzureClientID, "$ARM_CLIENT_ID") + r.Set(w.Config.AzureClientID, "$USERNAME") + r.Set(w.Config.AzureTenantID, "$ARM_TENANT_ID") + r.Set(w.Config.ActionsIDTokenRequestURL, "$ACTIONS_ID_TOKEN_REQUEST_URL") + r.Set(w.Config.ActionsIDTokenRequestToken, "$ACTIONS_ID_TOKEN_REQUEST_TOKEN") + r.Set(w.Config.AzureEnvironment, "$ARM_ENVIRONMENT") + r.Set(w.Config.ClientID, "$DATABRICKS_CLIENT_ID") + r.Set(w.Config.ClientSecret, "$DATABRICKS_CLIENT_SECRET") + r.Set(w.Config.DatabricksCliPath, "$DATABRICKS_CLI_PATH") + // This is set to words like "path" that happen too frequently + // r.Set(w.Config.AuthType, "$DATABRICKS_AUTH_TYPE") +} + +func PrepareReplacementsUser(t testutil.TestingT, r *ReplacementsContext, u iam.User) { + // There could be exact matches or overlap between different name fields, so sort them by length + // to ensure we match the largest one first and map them all to the same token + names := []string{ + u.DisplayName, + u.UserName, + iamutil.GetShortUserName(&u), + u.Name.FamilyName, + u.Name.GivenName, + } + if u.Name != nil { + names = append(names, u.Name.FamilyName) + names = append(names, u.Name.GivenName) + } + for _, val := range u.Emails { + names = append(names, val.Value) + } + stableSortReverseLength(names) + + for _, name := range names { + r.Set(name, "$USERNAME") + } + + for ind, val := range u.Groups { + r.Set(val.Value, fmt.Sprintf("$USER.Groups[%d]", ind)) + } + + r.Set(u.Id, "$USER.Id") + + for ind, val := range u.Roles { + r.Set(val.Value, fmt.Sprintf("$USER.Roles[%d]", ind)) + } +} + +func stableSortReverseLength(strs []string) { + slices.SortStableFunc(strs, func(a, b string) int { + return len(b) - len(a) + }) +} + +func NormalizeNewlines(input string) string { + output := strings.ReplaceAll(input, "\r\n", "\n") + return strings.ReplaceAll(output, "\r", "\n") +} diff --git a/internal/testcli/golden_test.go b/libs/testdiff/golden_test.go similarity index 92% rename from internal/testcli/golden_test.go rename to libs/testdiff/golden_test.go index 215bf33d3..0fc32be21 100644 --- a/internal/testcli/golden_test.go +++ b/libs/testdiff/golden_test.go @@ -1,4 +1,4 @@ -package testcli +package testdiff import ( "testing" From 1306e5ec6745b36acd7c17c9b3b4aea1f55d6807 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 30 Dec 2024 18:41:45 +0100 Subject: [PATCH 05/53] Add CODEOWNERS (#2055) Goal is to have DABs core team automatically added as reviewers so that you don't have to click manually. Based on this example: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#example-of-a-codeowners-file --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..76835de7d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @pietern @andrewnester @shreyas-goenka @denik From 1ce20a2612031a3d5c7b663203296957671e4f1d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 30 Dec 2024 19:39:33 +0100 Subject: [PATCH 06/53] lint.sh: read config for formatters; include gofmt (#2056) As suggested here: https://github.com/databricks/cli/pull/2051#discussion_r1899641273 --- lint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lint.sh b/lint.sh index c93d04c66..13efa9855 100755 --- a/lint.sh +++ b/lint.sh @@ -5,5 +5,5 @@ set -euo pipefail # However, running goimports first alone will actually fix some of the compilation issues. # Fixing formatting is also reasonable thing to do. # For this reason, this script runs golangci-lint in two stages: -golangci-lint run --fix --no-config --disable-all --enable gofumpt,goimports $@ +golangci-lint run --enable-only="gofmt,gofumpt,goimports" --fix $@ exec golangci-lint run --fix $@ From 511db52bb7a47270dd883bb490a8844f9b25fe76 Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:21:13 +0530 Subject: [PATCH 07/53] Remove unnecessary GET call in pipeline runner (#1850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes This GET API call is unnecessary and serves no purpose. Let's remove it. Noticed this when I was adding a unit test for the pipeline runner here: https://github.com/databricks/cli/pull/1849 ## Tests Manually. ### Case 1: The pipeline does not exist Before: ``` ➜ my_project git:(master) ✗ databricks bundle run my_project_pipeline -p dogfood Error: User shreyas.goenka@databricks.com does not have View permissions on pipeline 9941901a-e48b-4d04-b6ba-e0072ad126bg. ``` After: ``` ➜ my_project git:(master) ✗ cli bundle run my_project_pipeline -p dogfood Error: User shreyas.goenka@databricks.com does not have Run permissions on pipeline 9941901a-e48b-4d04-b6ba-e0072ad126bg. ``` ### Case 2: Pipeline exists Before: ``` ➜ my_project git:(master) ✗ databricks bundle run my_project_pipeline -p dogfood --restart Update URL: https://e2-dogfood.staging.cloud.databricks.com/#joblist/pipelines/9941901a-e48b-4d04-b6ba-e0072ad126bf/updates/0f988d62-9ec7-49f1-b429-5572ece3a9aa 2024-11-18T15:30:36.054Z update_progress INFO "Update 0f988d is WAITING_FOR_RESOURCES." ``` After: ``` ➜ my_project git:(master) ✗ cli bundle run my_project_pipeline -p dogfood --restart Update URL: https://e2-dogfood.staging.cloud.databricks.com/#joblist/pipelines/9941901a-e48b-4d04-b6ba-e0072ad126bf/updates/87b43350-6186-4a9b-9d0e-38da2ecf33ae 2024-11-18T15:28:27.144Z update_progress INFO "Update 87b433 is WAITING_FOR_RESOURCES." ``` --- bundle/run/pipeline.go | 5 ----- bundle/run/pipeline_test.go | 2 -- 2 files changed, 7 deletions(-) diff --git a/bundle/run/pipeline.go b/bundle/run/pipeline.go index a0e7d1e1e..d84015d76 100644 --- a/bundle/run/pipeline.go +++ b/bundle/run/pipeline.go @@ -90,11 +90,6 @@ func (r *pipelineRunner) Run(ctx context.Context, opts *Options) (output.RunOutp // Include resource key in logger. ctx = log.NewContext(ctx, log.GetLogger(ctx).With("resource", r.Key())) w := r.bundle.WorkspaceClient() - _, err := w.Pipelines.GetByPipelineId(ctx, pipelineID) - if err != nil { - log.Warnf(ctx, "Cannot get pipeline: %s", err) - return nil, err - } req, err := opts.Pipeline.toPayload(r.pipeline, pipelineID) if err != nil { diff --git a/bundle/run/pipeline_test.go b/bundle/run/pipeline_test.go index 66f9d86be..bfa0c5846 100644 --- a/bundle/run/pipeline_test.go +++ b/bundle/run/pipeline_test.go @@ -90,8 +90,6 @@ func TestPipelineRunnerRestart(t *testing.T) { PipelineId: "123", }).Return(mockWait, nil) - pipelineApi.EXPECT().GetByPipelineId(mock.Anything, "123").Return(&pipelines.GetPipelineResponse{}, nil) - // Mock runner starting a new update pipelineApi.EXPECT().StartUpdate(mock.Anything, pipelines.StartUpdate{ PipelineId: "123", From 3f523b45cc59aa2cb4fbbffdf267e8ea094b3ad6 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 31 Dec 2024 15:01:45 +0100 Subject: [PATCH 08/53] Fix lost diags across different mutators (#2057) ## Changes Fix cases where accumulated diagnostics are lost instead of being propagated further. In some cases it's not possible, add a comment there. ## Tests Existing tests --- bundle/artifacts/expand_globs.go | 2 +- bundle/config/mutator/resolve_resource_references.go | 1 + bundle/config/root.go | 3 ++- bundle/config/validate/folder_permissions.go | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bundle/artifacts/expand_globs.go b/bundle/artifacts/expand_globs.go index 7d44db0be..c0af7c69e 100644 --- a/bundle/artifacts/expand_globs.go +++ b/bundle/artifacts/expand_globs.go @@ -97,7 +97,7 @@ func (m *expandGlobs) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnost return dyn.SetByPath(v, base, dyn.V(output)) }) if err != nil { - return diag.FromErr(err) + diags = diags.Extend(diag.FromErr(err)) } return diags diff --git a/bundle/config/mutator/resolve_resource_references.go b/bundle/config/mutator/resolve_resource_references.go index bf902f928..20a5b6585 100644 --- a/bundle/config/mutator/resolve_resource_references.go +++ b/bundle/config/mutator/resolve_resource_references.go @@ -40,6 +40,7 @@ func (m *resolveResourceReferences) Apply(ctx context.Context, b *bundle.Bundle) }) } + // Note, diags are lost from all goroutines except the first one to return diag return diag.FromErr(errs.Wait()) } diff --git a/bundle/config/root.go b/bundle/config/root.go index 4b1467456..91c15fd9d 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -102,7 +102,8 @@ func LoadFromBytes(path string, raw []byte) (*Root, diag.Diagnostics) { // Convert normalized configuration tree to typed configuration. err = r.updateWithDynamicValue(v) if err != nil { - return nil, diag.Errorf("failed to load %s: %v", path, err) + diags = diags.Extend(diag.Errorf("failed to load %s: %v", path, err)) + return nil, diags } return &r, diags } diff --git a/bundle/config/validate/folder_permissions.go b/bundle/config/validate/folder_permissions.go index aa89a0551..5b28d599e 100644 --- a/bundle/config/validate/folder_permissions.go +++ b/bundle/config/validate/folder_permissions.go @@ -36,7 +36,8 @@ func (f *folderPermissions) Apply(ctx context.Context, b bundle.ReadOnlyBundle) } if err := g.Wait(); err != nil { - return diag.FromErr(err) + // Note, only diag from first coroutine is captured, others are lost + diags = diags.Extend(diag.FromErr(err)) } for _, r := range results { From 3f75240a562d88a9411cdd50a2ba24139ca094b7 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jan 2025 10:49:21 +0100 Subject: [PATCH 09/53] Improve test output to include correct location (#2058) ## Changes - Add t.Helper() in testcli-related helpers, this ensures that output is attributed correctly to test case and not to the helper. - Modify testlcli.Run() to run process in foreground. This is needed for t.Helper to work. - Extend a few assertions with message to help attribute it to proper helper where needed. ## Tests Manually reviewed test output. Before: ``` + go test --timeout 3h -v -run TestDefaultPython/3.9 ./integration/bundle/ === RUN TestDefaultPython === RUN TestDefaultPython/3.9 workspace.go:26: aws golden.go:14: run args: [bundle, init, default-python, --config-file, config.json] runner.go:206: [databricks stderr]: runner.go:206: [databricks stderr]: Welcome to the default Python template for Databricks Asset Bundles! ... testdiff.go:23: Error Trace: /Users/denis.bilenko/work/cli/libs/testdiff/testdiff.go:23 /Users/denis.bilenko/work/cli/libs/testdiff/golden.go:43 /Users/denis.bilenko/work/cli/internal/testcli/golden.go:23 /Users/denis.bilenko/work/cli/integration/bundle/init_default_python_test.go:92 /Users/denis.bilenko/work/cli/integration/bundle/init_default_python_test.go:45 ... ``` After: ``` + go test --timeout 3h -v -run TestDefaultPython/3.9 ./integration/bundle/ === RUN TestDefaultPython === RUN TestDefaultPython/3.9 init_default_python_test.go:51: CLOUD_ENV=aws init_default_python_test.go:92: args: bundle, init, default-python, --config-file, config.json init_default_python_test.go:92: stderr: init_default_python_test.go:92: stderr: Welcome to the default Python template for Databricks Asset Bundles! ... init_default_python_test.go:92: Error Trace: /Users/denis.bilenko/work/cli/libs/testdiff/testdiff.go:24 /Users/denis.bilenko/work/cli/libs/testdiff/golden.go:46 /Users/denis.bilenko/work/cli/internal/testcli/golden.go:23 /Users/denis.bilenko/work/cli/integration/bundle/init_default_python_test.go:92 /Users/denis.bilenko/work/cli/integration/bundle/init_default_python_test.go:45 ... ``` --- integration/internal/acc/workspace.go | 14 +++++--- internal/testcli/golden.go | 5 +-- internal/testcli/runner.go | 47 ++++++++++++++++++++++++--- internal/testutil/interface.go | 2 ++ libs/testdiff/golden.go | 11 +++++-- libs/testdiff/testdiff.go | 4 ++- 6 files changed, 69 insertions(+), 14 deletions(-) diff --git a/integration/internal/acc/workspace.go b/integration/internal/acc/workspace.go index 2f8a5b8e7..64deda7c1 100644 --- a/integration/internal/acc/workspace.go +++ b/integration/internal/acc/workspace.go @@ -21,9 +21,10 @@ type WorkspaceT struct { } func WorkspaceTest(t testutil.TestingT) (context.Context, *WorkspaceT) { + t.Helper() loadDebugEnvIfRunFromIDE(t, "workspace") - t.Log(testutil.GetEnvOrSkipTest(t, "CLOUD_ENV")) + t.Logf("CLOUD_ENV=%s", testutil.GetEnvOrSkipTest(t, "CLOUD_ENV")) w, err := databricks.NewWorkspaceClient() require.NoError(t, err) @@ -41,9 +42,10 @@ func WorkspaceTest(t testutil.TestingT) (context.Context, *WorkspaceT) { // Run the workspace test only on UC workspaces. func UcWorkspaceTest(t testutil.TestingT) (context.Context, *WorkspaceT) { + t.Helper() loadDebugEnvIfRunFromIDE(t, "workspace") - t.Log(testutil.GetEnvOrSkipTest(t, "CLOUD_ENV")) + t.Logf("CLOUD_ENV=%s", testutil.GetEnvOrSkipTest(t, "CLOUD_ENV")) if os.Getenv("TEST_METASTORE_ID") == "" { t.Skipf("Skipping on non-UC workspaces") @@ -67,19 +69,21 @@ func UcWorkspaceTest(t testutil.TestingT) (context.Context, *WorkspaceT) { } func (t *WorkspaceT) TestClusterID() string { + t.Helper() clusterID := testutil.GetEnvOrSkipTest(t, "TEST_BRICKS_CLUSTER_ID") err := t.W.Clusters.EnsureClusterIsRunning(t.ctx, clusterID) - require.NoError(t, err) + require.NoError(t, err, "Unexpected error from EnsureClusterIsRunning for clusterID=%s", clusterID) return clusterID } func (t *WorkspaceT) RunPython(code string) (string, error) { + t.Helper() var err error // Create command executor only once per test. if t.exec == nil { t.exec, err = t.W.CommandExecution.Start(t.ctx, t.TestClusterID(), compute.LanguagePython) - require.NoError(t, err) + require.NoError(t, err, "Unexpected error from CommandExecution.Start(clusterID=%v)", t.TestClusterID()) t.Cleanup(func() { err := t.exec.Destroy(t.ctx) @@ -88,7 +92,7 @@ func (t *WorkspaceT) RunPython(code string) (string, error) { } results, err := t.exec.Execute(t.ctx, code) - require.NoError(t, err) + require.NoError(t, err, "Unexpected error from Execute(%v)", code) require.NotEqual(t, compute.ResultTypeError, results.ResultType, results.Cause) output, ok := results.Data.(string) require.True(t, ok, "unexpected type %T", results.Data) diff --git a/internal/testcli/golden.go b/internal/testcli/golden.go index a16f42c72..669cc2f9b 100644 --- a/internal/testcli/golden.go +++ b/internal/testcli/golden.go @@ -3,7 +3,6 @@ package testcli import ( "context" "fmt" - "strings" "github.com/databricks/cli/internal/testutil" "github.com/databricks/cli/libs/testdiff" @@ -11,7 +10,7 @@ import ( ) func captureOutput(t testutil.TestingT, ctx context.Context, args []string) string { - t.Logf("run args: [%s]", strings.Join(args, ", ")) + t.Helper() r := NewRunner(t, ctx, args...) stdout, stderr, err := r.Run() assert.NoError(t, err) @@ -19,11 +18,13 @@ func captureOutput(t testutil.TestingT, ctx context.Context, args []string) stri } func AssertOutput(t testutil.TestingT, ctx context.Context, args []string, expectedPath string) { + t.Helper() out := captureOutput(t, ctx, args) testdiff.AssertOutput(t, ctx, out, fmt.Sprintf("Output from %v", args), expectedPath) } func AssertOutputJQ(t testutil.TestingT, ctx context.Context, args []string, expectedPath string, ignorePaths []string) { + t.Helper() out := captureOutput(t, ctx, args) testdiff.AssertOutputJQ(t, ctx, out, fmt.Sprintf("Output from %v", args), expectedPath, ignorePaths) } diff --git a/internal/testcli/runner.go b/internal/testcli/runner.go index 95073b57c..52decad2c 100644 --- a/internal/testcli/runner.go +++ b/internal/testcli/runner.go @@ -69,6 +69,7 @@ func consumeLines(ctx context.Context, wg *sync.WaitGroup, r io.Reader) <-chan s } func (r *Runner) registerFlagCleanup(c *cobra.Command) { + r.Helper() // Find target command that will be run. Example: if the command run is `databricks fs cp`, // target command corresponds to `cp` targetCmd, _, err := c.Find(r.args) @@ -230,13 +231,48 @@ func (r *Runner) RunBackground() { } func (r *Runner) Run() (bytes.Buffer, bytes.Buffer, error) { - r.RunBackground() - err := <-r.errch - return r.stdout, r.stderr, err + r.Helper() + var stdout, stderr bytes.Buffer + ctx := cmdio.NewContext(r.ctx, &cmdio.Logger{ + Mode: flags.ModeAppend, + Reader: bufio.Reader{}, + Writer: &stderr, + }) + + cli := cmd.New(ctx) + cli.SetOut(&stdout) + cli.SetErr(&stderr) + cli.SetArgs(r.args) + + r.Logf(" args: %s", strings.Join(r.args, ", ")) + + err := root.Execute(ctx, cli) + if err != nil { + r.Logf(" error: %s", err) + } + + if stdout.Len() > 0 { + // Make a copy of the buffer such that it remains "unread". + scanner := bufio.NewScanner(bytes.NewBuffer(stdout.Bytes())) + for scanner.Scan() { + r.Logf("stdout: %s", scanner.Text()) + } + } + + if stderr.Len() > 0 { + // Make a copy of the buffer such that it remains "unread". + scanner := bufio.NewScanner(bytes.NewBuffer(stderr.Bytes())) + for scanner.Scan() { + r.Logf("stderr: %s", scanner.Text()) + } + } + + return stdout, stderr, err } // Like [require.Eventually] but errors if the underlying command has failed. func (r *Runner) Eventually(condition func() bool, waitFor, tick time.Duration, msgAndArgs ...any) { + r.Helper() ch := make(chan bool, 1) timer := time.NewTimer(waitFor) @@ -269,12 +305,14 @@ func (r *Runner) Eventually(condition func() bool, waitFor, tick time.Duration, } func (r *Runner) RunAndExpectOutput(heredoc string) { + r.Helper() stdout, _, err := r.Run() require.NoError(r, err) require.Equal(r, cmdio.Heredoc(heredoc), strings.TrimSpace(stdout.String())) } func (r *Runner) RunAndParseJSON(v any) { + r.Helper() stdout, _, err := r.Run() require.NoError(r, err) err = json.Unmarshal(stdout.Bytes(), &v) @@ -291,7 +329,7 @@ func NewRunner(t testutil.TestingT, ctx context.Context, args ...string) *Runner } func RequireSuccessfulRun(t testutil.TestingT, ctx context.Context, args ...string) (bytes.Buffer, bytes.Buffer) { - t.Logf("run args: [%s]", strings.Join(args, ", ")) + t.Helper() r := NewRunner(t, ctx, args...) stdout, stderr, err := r.Run() require.NoError(t, err) @@ -299,6 +337,7 @@ func RequireSuccessfulRun(t testutil.TestingT, ctx context.Context, args ...stri } func RequireErrorRun(t testutil.TestingT, ctx context.Context, args ...string) (bytes.Buffer, bytes.Buffer, error) { + t.Helper() r := NewRunner(t, ctx, args...) stdout, stderr, err := r.Run() require.Error(t, err) diff --git a/internal/testutil/interface.go b/internal/testutil/interface.go index 2c3004800..97441212d 100644 --- a/internal/testutil/interface.go +++ b/internal/testutil/interface.go @@ -24,4 +24,6 @@ type TestingT interface { Setenv(key, value string) TempDir() string + + Helper() } diff --git a/libs/testdiff/golden.go b/libs/testdiff/golden.go index c338ac9e5..b67eb50a9 100644 --- a/libs/testdiff/golden.go +++ b/libs/testdiff/golden.go @@ -19,22 +19,25 @@ import ( var OverwriteMode = os.Getenv("TESTS_OUTPUT") == "OVERWRITE" func ReadFile(t testutil.TestingT, ctx context.Context, filename string) string { + t.Helper() data, err := os.ReadFile(filename) if os.IsNotExist(err) { return "" } - assert.NoError(t, err) + assert.NoError(t, err, "Failed to read %s", filename) // On CI, on Windows \n in the file somehow end up as \r\n return NormalizeNewlines(string(data)) } func WriteFile(t testutil.TestingT, filename, data string) { + t.Helper() t.Logf("Overwriting %s", filename) err := os.WriteFile(filename, []byte(data), 0o644) - assert.NoError(t, err) + assert.NoError(t, err, "Failed to write %s", filename) } func AssertOutput(t testutil.TestingT, ctx context.Context, out, outTitle, expectedPath string) { + t.Helper() expected := ReadFile(t, ctx, expectedPath) out = ReplaceOutput(t, ctx, out) @@ -49,6 +52,7 @@ func AssertOutput(t testutil.TestingT, ctx context.Context, out, outTitle, expec } func AssertOutputJQ(t testutil.TestingT, ctx context.Context, out, outTitle, expectedPath string, ignorePaths []string) { + t.Helper() expected := ReadFile(t, ctx, expectedPath) out = ReplaceOutput(t, ctx, out) @@ -69,6 +73,7 @@ var ( ) func ReplaceOutput(t testutil.TestingT, ctx context.Context, out string) string { + t.Helper() out = NormalizeNewlines(out) replacements := GetReplacementsMap(ctx) if replacements == nil { @@ -136,6 +141,7 @@ func GetReplacementsMap(ctx context.Context) *ReplacementsContext { } func PrepareReplacements(t testutil.TestingT, r *ReplacementsContext, w *databricks.WorkspaceClient) { + t.Helper() // in some clouds (gcp) w.Config.Host includes "https://" prefix in others it's really just a host (azure) host := strings.TrimPrefix(strings.TrimPrefix(w.Config.Host, "http://"), "https://") r.Set(host, "$DATABRICKS_HOST") @@ -167,6 +173,7 @@ func PrepareReplacements(t testutil.TestingT, r *ReplacementsContext, w *databri } func PrepareReplacementsUser(t testutil.TestingT, r *ReplacementsContext, u iam.User) { + t.Helper() // There could be exact matches or overlap between different name fields, so sort them by length // to ensure we match the largest one first and map them all to the same token names := []string{ diff --git a/libs/testdiff/testdiff.go b/libs/testdiff/testdiff.go index 1e1df727a..71b781362 100644 --- a/libs/testdiff/testdiff.go +++ b/libs/testdiff/testdiff.go @@ -18,9 +18,10 @@ func UnifiedDiff(filename1, filename2, s1, s2 string) string { } func AssertEqualTexts(t testutil.TestingT, filename1, filename2, expected, out string) { + t.Helper() if len(out) < 1000 && len(expected) < 1000 { // This shows full strings + diff which could be useful when debugging newlines - assert.Equal(t, expected, out) + assert.Equal(t, expected, out, "%s vs %s", filename1, filename2) } else { // only show diff for large texts diff := UnifiedDiff(filename1, filename2, expected, out) @@ -29,6 +30,7 @@ func AssertEqualTexts(t testutil.TestingT, filename1, filename2, expected, out s } func AssertEqualJQ(t testutil.TestingT, expectedName, outName, expected, out string, ignorePaths []string) { + t.Helper() patch, err := jsondiff.CompareJSON([]byte(expected), []byte(out)) if err != nil { t.Logf("CompareJSON error for %s vs %s: %s (fallback to textual comparison)", outName, expectedName, err) From d7f69f6a5dc80450498119a7e2a60eecfc3f0cab Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jan 2025 11:31:35 +0100 Subject: [PATCH 10/53] Upgrade golangci-lint to v1.63.1 (#2064) Upgrade your laptops with: brew install golangci-lint This has a lot more autofixes, which makes it easier to adopt those linters. https://golangci-lint.run/product/changelog/#v1630 --- .github/workflows/push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index a51927594..c737993aa 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -75,7 +75,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.62.2 + version: v1.63.1 args: --timeout=15m validate-bundle-schema: From c262b75c3d0e48839fe327697e2758d3120d780e Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jan 2025 11:33:06 +0100 Subject: [PATCH 11/53] Make lint.sh to run golangci-lint only once in the best case (#2062) Follow up to #2051 and #2056. Running golangci-lint twice always is measurably slower (1.5s vs 2.5s), so only run it twice in case it is necessary. --- lint.sh | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lint.sh b/lint.sh index 13efa9855..1f881eaf7 100755 --- a/lint.sh +++ b/lint.sh @@ -1,9 +1,14 @@ #!/bin/bash -set -euo pipefail +set -uo pipefail # With golangci-lint, if there are any compliation issues, then formatters' autofix won't be applied. # https://github.com/golangci/golangci-lint/issues/5257 -# However, running goimports first alone will actually fix some of the compilation issues. -# Fixing formatting is also reasonable thing to do. -# For this reason, this script runs golangci-lint in two stages: -golangci-lint run --enable-only="gofmt,gofumpt,goimports" --fix $@ -exec golangci-lint run --fix $@ + +golangci-lint run --fix "$@" +lint_exit_code=$? + +if [ $lint_exit_code -ne 0 ]; then + # These linters work in presence of compilation issues when run alone, so let's get these fixes at least. + golangci-lint run --enable-only="gofmt,gofumpt,goimports" --fix "$@" +fi + +exit $lint_exit_code From b053bfb4de577d8ee2d4058672a694af69d80891 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 2 Jan 2025 11:41:55 +0100 Subject: [PATCH 12/53] Verify that the bundle schema is up to date in CI (#2061) ## Changes I noticed a diff in the schema in #2052. This check should be performed automatically. ## Tests This PR includes a commit that changes the schema to check that the workflow actually fails. --- .github/workflows/push.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index c737993aa..25bd5f4d6 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -90,6 +90,13 @@ jobs: with: go-version: 1.23.4 + - name: Verify that the schema is up to date + run: | + if ! ( make schema && git diff --exit-code ); then + echo "The schema is not up to date. Please run 'make schema' and commit the changes." + exit 1 + fi + # Github repo: https://github.com/ajv-validator/ajv-cli - name: Install ajv-cli run: npm install -g ajv-cli@5.0.0 From 0b80784df759673d0ccc3874d19d6662ff11ac54 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jan 2025 12:03:41 +0100 Subject: [PATCH 13/53] Enable testifylint and fix the issues (#2065) ## Changes - Enable new linter: testifylint. - Apply fixes with --fix. - Fix remaining issues (mostly with aider). There were 2 cases we --fix did the wrong thing - this seems to a be a bug in linter: https://github.com/Antonboom/testifylint/issues/210 Nonetheless, I kept that check enabled, it seems useful, just need to be fixed manually after autofix. ## Tests Existing tests --- .golangci.yaml | 8 +++- bundle/bundle_test.go | 3 +- .../configure_dashboard_defaults_test.go | 6 +-- .../config/mutator/default_queueing_test.go | 10 ++-- .../mutator/environments_compat_test.go | 4 +- bundle/config/mutator/merge_job_tasks_test.go | 4 +- .../mutator/process_target_mode_test.go | 12 ++--- .../resolve_variable_references_test.go | 6 +-- .../mutator/rewrite_workspace_prefix_test.go | 2 +- bundle/config/mutator/set_variables_test.go | 8 ++-- bundle/config/resources_test.go | 6 +-- bundle/config/validate/files_to_sync_test.go | 4 +- .../validate/job_cluster_key_defined_test.go | 10 ++-- bundle/context_test.go | 2 +- bundle/deploy/state_pull_test.go | 3 +- bundle/deploy/state_update_test.go | 12 ++--- bundle/deploy/terraform/convert_test.go | 10 ++-- bundle/deploy/terraform/init_test.go | 4 +- bundle/internal/schema/main_test.go | 3 +- bundle/libraries/filer_volume_test.go | 4 +- bundle/libraries/helpers_test.go | 12 ++--- bundle/permissions/filter_test.go | 28 +++++------ bundle/permissions/mutator_test.go | 2 +- .../permission_diagnostics_test.go | 2 +- bundle/run/job_test.go | 2 +- bundle/tests/complex_variables_test.go | 2 +- bundle/tests/environment_git_test.go | 5 +- bundle/tests/environment_overrides_test.go | 8 ++-- .../environments_job_and_pipeline_test.go | 6 +-- bundle/tests/git_test.go | 5 +- bundle/tests/issue_1828_test.go | 4 +- bundle/tests/job_and_pipeline_test.go | 6 +-- bundle/tests/job_cluster_key_test.go | 6 +-- bundle/tests/model_serving_endpoint_test.go | 2 +- bundle/tests/override_job_tasks_test.go | 28 +++++------ bundle/tests/presets_test.go | 4 +- bundle/tests/python_wheel_test.go | 10 ++-- bundle/tests/quality_monitor_test.go | 2 +- bundle/tests/registered_model_test.go | 2 +- .../sync_include_exclude_no_matches_test.go | 20 ++++---- bundle/tests/sync_test.go | 4 +- cmd/bundle/generate/dashboard_test.go | 2 +- cmd/bundle/generate/generate_test.go | 3 +- cmd/configure/configure_test.go | 4 +- cmd/labs/github/ref_test.go | 9 ++-- cmd/labs/github/releases_test.go | 5 +- cmd/labs/github/repositories_test.go | 5 +- cmd/labs/localcache/jsonfile_test.go | 2 +- cmd/labs/project/installer_test.go | 17 +++---- integration/bundle/artifacts_test.go | 12 ++--- integration/bundle/deploy_test.go | 3 +- integration/bundle/destroy_test.go | 5 +- integration/cmd/fs/ls_test.go | 15 +++--- integration/cmd/fs/mkdir_test.go | 8 ++-- integration/cmd/sync/sync_test.go | 2 +- integration/libs/filer/filer_test.go | 46 +++++++++---------- libs/auth/cache/file_test.go | 2 +- libs/databrickscfg/ops_test.go | 2 +- libs/databrickscfg/profile/file_test.go | 8 ++-- libs/errs/aggregate_test.go | 19 ++++---- libs/filer/completer/completer_test.go | 12 ++--- libs/filer/fs_test.go | 4 +- libs/fileset/glob_test.go | 2 +- libs/flags/json_flag_test.go | 8 ++-- libs/folders/folders_test.go | 2 +- libs/jsonschema/utils_test.go | 4 +- libs/notebook/detect_test.go | 5 +- libs/process/background_test.go | 2 +- libs/process/forwarded_test.go | 4 +- libs/process/opts_test.go | 2 +- libs/python/interpreters_unix_test.go | 2 +- libs/sync/snapshot_test.go | 24 +++++----- libs/sync/sync_test.go | 12 ++--- libs/template/config_test.go | 8 ++-- libs/template/helpers_test.go | 2 +- libs/template/renderer_test.go | 8 ++-- libs/vfs/filer_test.go | 9 ++-- 77 files changed, 277 insertions(+), 283 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 9e69e5146..fd83e0882 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -11,6 +11,7 @@ linters: - gofmt - gofumpt - goimports + - testifylint linters-settings: govet: enable-all: true @@ -32,7 +33,10 @@ linters-settings: gofumpt: module-path: github.com/databricks/cli extra-rules: true - #goimports: - # local-prefixes: github.com/databricks/cli + testifylint: + enable-all: true + disable: + # good check, but we have too many assert.(No)?Errorf? so excluding for now + - require-error issues: exclude-dirs-use-default: false # recommended by docs https://golangci-lint.run/usage/false-positives/ diff --git a/bundle/bundle_test.go b/bundle/bundle_test.go index 1c3102357..d52088e34 100644 --- a/bundle/bundle_test.go +++ b/bundle/bundle_test.go @@ -2,7 +2,6 @@ package bundle import ( "context" - "errors" "io/fs" "os" "path/filepath" @@ -16,7 +15,7 @@ import ( func TestLoadNotExists(t *testing.T) { b, err := Load(context.Background(), "/doesntexist") - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) assert.Nil(t, b) } diff --git a/bundle/config/mutator/configure_dashboard_defaults_test.go b/bundle/config/mutator/configure_dashboard_defaults_test.go index 2234f9a73..9794d355c 100644 --- a/bundle/config/mutator/configure_dashboard_defaults_test.go +++ b/bundle/config/mutator/configure_dashboard_defaults_test.go @@ -109,19 +109,19 @@ func TestConfigureDashboardDefaultsEmbedCredentials(t *testing.T) { // Set to true; still true. v, err = dyn.Get(b.Config.Value(), "resources.dashboards.d1.embed_credentials") if assert.NoError(t, err) { - assert.Equal(t, true, v.MustBool()) + assert.True(t, v.MustBool()) } // Set to false; still false. v, err = dyn.Get(b.Config.Value(), "resources.dashboards.d2.embed_credentials") if assert.NoError(t, err) { - assert.Equal(t, false, v.MustBool()) + assert.False(t, v.MustBool()) } // Not set; now false. v, err = dyn.Get(b.Config.Value(), "resources.dashboards.d3.embed_credentials") if assert.NoError(t, err) { - assert.Equal(t, false, v.MustBool()) + assert.False(t, v.MustBool()) } // No valid dashboard; no change. diff --git a/bundle/config/mutator/default_queueing_test.go b/bundle/config/mutator/default_queueing_test.go index d3621663b..4c521812e 100644 --- a/bundle/config/mutator/default_queueing_test.go +++ b/bundle/config/mutator/default_queueing_test.go @@ -28,8 +28,8 @@ func TestDefaultQueueingApplyNoJobs(t *testing.T) { }, } d := bundle.Apply(context.Background(), b, DefaultQueueing()) - assert.Len(t, d, 0) - assert.Len(t, b.Config.Resources.Jobs, 0) + assert.Empty(t, d) + assert.Empty(t, b.Config.Resources.Jobs) } func TestDefaultQueueingApplyJobsAlreadyEnabled(t *testing.T) { @@ -47,7 +47,7 @@ func TestDefaultQueueingApplyJobsAlreadyEnabled(t *testing.T) { }, } d := bundle.Apply(context.Background(), b, DefaultQueueing()) - assert.Len(t, d, 0) + assert.Empty(t, d) assert.True(t, b.Config.Resources.Jobs["job"].Queue.Enabled) } @@ -66,7 +66,7 @@ func TestDefaultQueueingApplyEnableQueueing(t *testing.T) { }, } d := bundle.Apply(context.Background(), b, DefaultQueueing()) - assert.Len(t, d, 0) + assert.Empty(t, d) assert.NotNil(t, b.Config.Resources.Jobs["job"].Queue) assert.True(t, b.Config.Resources.Jobs["job"].Queue.Enabled) } @@ -96,7 +96,7 @@ func TestDefaultQueueingApplyWithMultipleJobs(t *testing.T) { }, } d := bundle.Apply(context.Background(), b, DefaultQueueing()) - assert.Len(t, d, 0) + assert.Empty(t, d) assert.False(t, b.Config.Resources.Jobs["job1"].Queue.Enabled) assert.True(t, b.Config.Resources.Jobs["job2"].Queue.Enabled) assert.True(t, b.Config.Resources.Jobs["job3"].Queue.Enabled) diff --git a/bundle/config/mutator/environments_compat_test.go b/bundle/config/mutator/environments_compat_test.go index 8a2129847..11facf9fb 100644 --- a/bundle/config/mutator/environments_compat_test.go +++ b/bundle/config/mutator/environments_compat_test.go @@ -44,7 +44,7 @@ func TestEnvironmentsToTargetsWithEnvironmentsDefined(t *testing.T) { diags := bundle.Apply(context.Background(), b, mutator.EnvironmentsToTargets()) require.NoError(t, diags.Error()) - assert.Len(t, b.Config.Environments, 0) + assert.Empty(t, b.Config.Environments) assert.Len(t, b.Config.Targets, 1) } @@ -61,6 +61,6 @@ func TestEnvironmentsToTargetsWithTargetsDefined(t *testing.T) { diags := bundle.Apply(context.Background(), b, mutator.EnvironmentsToTargets()) require.NoError(t, diags.Error()) - assert.Len(t, b.Config.Environments, 0) + assert.Empty(t, b.Config.Environments) assert.Len(t, b.Config.Targets, 1) } diff --git a/bundle/config/mutator/merge_job_tasks_test.go b/bundle/config/mutator/merge_job_tasks_test.go index a9dae1e10..e6675eefb 100644 --- a/bundle/config/mutator/merge_job_tasks_test.go +++ b/bundle/config/mutator/merge_job_tasks_test.go @@ -74,8 +74,8 @@ func TestMergeJobTasks(t *testing.T) { assert.Equal(t, "i3.2xlarge", cluster.NodeTypeId) assert.Equal(t, 4, cluster.NumWorkers) assert.Len(t, task0.Libraries, 2) - assert.Equal(t, task0.Libraries[0].Whl, "package1") - assert.Equal(t, task0.Libraries[1].Pypi.Package, "package2") + assert.Equal(t, "package1", task0.Libraries[0].Whl) + assert.Equal(t, "package2", task0.Libraries[1].Pypi.Package) // This task was left untouched. task1 := j.Tasks[1].NewCluster diff --git a/bundle/config/mutator/process_target_mode_test.go b/bundle/config/mutator/process_target_mode_test.go index 14d524416..e1aa9e59b 100644 --- a/bundle/config/mutator/process_target_mode_test.go +++ b/bundle/config/mutator/process_target_mode_test.go @@ -163,18 +163,18 @@ func TestProcessTargetModeDevelopment(t *testing.T) { // Job 1 assert.Equal(t, "[dev lennart] job1", b.Config.Resources.Jobs["job1"].Name) - assert.Equal(t, b.Config.Resources.Jobs["job1"].Tags["existing"], "tag") - assert.Equal(t, b.Config.Resources.Jobs["job1"].Tags["dev"], "lennart") - assert.Equal(t, b.Config.Resources.Jobs["job1"].Schedule.PauseStatus, jobs.PauseStatusPaused) + assert.Equal(t, "tag", b.Config.Resources.Jobs["job1"].Tags["existing"]) + assert.Equal(t, "lennart", b.Config.Resources.Jobs["job1"].Tags["dev"]) + assert.Equal(t, jobs.PauseStatusPaused, b.Config.Resources.Jobs["job1"].Schedule.PauseStatus) // Job 2 assert.Equal(t, "[dev lennart] job2", b.Config.Resources.Jobs["job2"].Name) - assert.Equal(t, b.Config.Resources.Jobs["job2"].Tags["dev"], "lennart") - assert.Equal(t, b.Config.Resources.Jobs["job2"].Schedule.PauseStatus, jobs.PauseStatusUnpaused) + assert.Equal(t, "lennart", b.Config.Resources.Jobs["job2"].Tags["dev"]) + assert.Equal(t, jobs.PauseStatusUnpaused, b.Config.Resources.Jobs["job2"].Schedule.PauseStatus) // Pipeline 1 assert.Equal(t, "[dev lennart] pipeline1", b.Config.Resources.Pipelines["pipeline1"].Name) - assert.Equal(t, false, b.Config.Resources.Pipelines["pipeline1"].Continuous) + assert.False(t, b.Config.Resources.Pipelines["pipeline1"].Continuous) assert.True(t, b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development) // Experiment 1 diff --git a/bundle/config/mutator/resolve_variable_references_test.go b/bundle/config/mutator/resolve_variable_references_test.go index 7bb6f11a0..07972ecf4 100644 --- a/bundle/config/mutator/resolve_variable_references_test.go +++ b/bundle/config/mutator/resolve_variable_references_test.go @@ -185,11 +185,11 @@ func TestResolveVariableReferencesForPrimitiveNonStringFields(t *testing.T) { // Apply for the variable prefix. This should resolve the variables to their values. diags = bundle.Apply(context.Background(), b, ResolveVariableReferences("variables")) require.NoError(t, diags.Error()) - assert.Equal(t, true, b.Config.Resources.Jobs["job1"].JobSettings.NotificationSettings.NoAlertForCanceledRuns) - assert.Equal(t, true, b.Config.Resources.Jobs["job1"].JobSettings.NotificationSettings.NoAlertForSkippedRuns) + assert.True(t, b.Config.Resources.Jobs["job1"].JobSettings.NotificationSettings.NoAlertForCanceledRuns) + assert.True(t, b.Config.Resources.Jobs["job1"].JobSettings.NotificationSettings.NoAlertForSkippedRuns) assert.Equal(t, 1, b.Config.Resources.Jobs["job1"].JobSettings.Tasks[0].NewCluster.Autoscale.MinWorkers) assert.Equal(t, 2, b.Config.Resources.Jobs["job1"].JobSettings.Tasks[0].NewCluster.Autoscale.MaxWorkers) - assert.Equal(t, 0.5, b.Config.Resources.Jobs["job1"].JobSettings.Tasks[0].NewCluster.AzureAttributes.SpotBidMaxPrice) + assert.InDelta(t, 0.5, b.Config.Resources.Jobs["job1"].JobSettings.Tasks[0].NewCluster.AzureAttributes.SpotBidMaxPrice, 0.0001) } func TestResolveComplexVariable(t *testing.T) { diff --git a/bundle/config/mutator/rewrite_workspace_prefix_test.go b/bundle/config/mutator/rewrite_workspace_prefix_test.go index 48973a4cf..099738c02 100644 --- a/bundle/config/mutator/rewrite_workspace_prefix_test.go +++ b/bundle/config/mutator/rewrite_workspace_prefix_test.go @@ -71,7 +71,7 @@ func TestNoWorkspacePrefixUsed(t *testing.T) { } for _, d := range diags { - require.Equal(t, d.Severity, diag.Warning) + require.Equal(t, diag.Warning, d.Severity) require.Contains(t, expectedErrors, d.Summary) delete(expectedErrors, d.Summary) } diff --git a/bundle/config/mutator/set_variables_test.go b/bundle/config/mutator/set_variables_test.go index d9719793f..07a5c8214 100644 --- a/bundle/config/mutator/set_variables_test.go +++ b/bundle/config/mutator/set_variables_test.go @@ -30,7 +30,7 @@ func TestSetVariableFromProcessEnvVar(t *testing.T) { err = convert.ToTyped(&variable, v) require.NoError(t, err) - assert.Equal(t, variable.Value, "process-env") + assert.Equal(t, "process-env", variable.Value) } func TestSetVariableUsingDefaultValue(t *testing.T) { @@ -48,7 +48,7 @@ func TestSetVariableUsingDefaultValue(t *testing.T) { err = convert.ToTyped(&variable, v) require.NoError(t, err) - assert.Equal(t, variable.Value, "default") + assert.Equal(t, "default", variable.Value) } func TestSetVariableWhenAlreadyAValueIsAssigned(t *testing.T) { @@ -70,7 +70,7 @@ func TestSetVariableWhenAlreadyAValueIsAssigned(t *testing.T) { err = convert.ToTyped(&variable, v) require.NoError(t, err) - assert.Equal(t, variable.Value, "assigned-value") + assert.Equal(t, "assigned-value", variable.Value) } func TestSetVariableEnvVarValueDoesNotOverridePresetValue(t *testing.T) { @@ -95,7 +95,7 @@ func TestSetVariableEnvVarValueDoesNotOverridePresetValue(t *testing.T) { err = convert.ToTyped(&variable, v) require.NoError(t, err) - assert.Equal(t, variable.Value, "assigned-value") + assert.Equal(t, "assigned-value", variable.Value) } func TestSetVariablesErrorsIfAValueCouldNotBeResolved(t *testing.T) { diff --git a/bundle/config/resources_test.go b/bundle/config/resources_test.go index 2d05acf3e..3da645585 100644 --- a/bundle/config/resources_test.go +++ b/bundle/config/resources_test.go @@ -37,11 +37,11 @@ func TestCustomMarshallerIsImplemented(t *testing.T) { field := rt.Field(i) // Fields in Resources are expected be of the form map[string]*resourceStruct - assert.Equal(t, field.Type.Kind(), reflect.Map, "Resource %s is not a map", field.Name) + assert.Equal(t, reflect.Map, field.Type.Kind(), "Resource %s is not a map", field.Name) kt := field.Type.Key() - assert.Equal(t, kt.Kind(), reflect.String, "Resource %s is not a map with string keys", field.Name) + assert.Equal(t, reflect.String, kt.Kind(), "Resource %s is not a map with string keys", field.Name) vt := field.Type.Elem() - assert.Equal(t, vt.Kind(), reflect.Ptr, "Resource %s is not a map with pointer values", field.Name) + assert.Equal(t, reflect.Ptr, vt.Kind(), "Resource %s is not a map with pointer values", field.Name) // Marshalling a resourceStruct will panic if resourceStruct does not have a custom marshaller // This is because resourceStruct embeds a Go SDK struct that implements diff --git a/bundle/config/validate/files_to_sync_test.go b/bundle/config/validate/files_to_sync_test.go index d6a1ed59a..dd40295c3 100644 --- a/bundle/config/validate/files_to_sync_test.go +++ b/bundle/config/validate/files_to_sync_test.go @@ -87,7 +87,7 @@ func TestFilesToSync_EverythingIgnored(t *testing.T) { ctx := context.Background() rb := bundle.ReadOnly(b) diags := bundle.ApplyReadOnly(ctx, rb, FilesToSync()) - require.Equal(t, 1, len(diags)) + require.Len(t, diags, 1) assert.Equal(t, diag.Warning, diags[0].Severity) assert.Equal(t, "There are no files to sync, please check your .gitignore", diags[0].Summary) } @@ -101,7 +101,7 @@ func TestFilesToSync_EverythingExcluded(t *testing.T) { ctx := context.Background() rb := bundle.ReadOnly(b) diags := bundle.ApplyReadOnly(ctx, rb, FilesToSync()) - require.Equal(t, 1, len(diags)) + require.Len(t, diags, 1) assert.Equal(t, diag.Warning, diags[0].Severity) assert.Equal(t, "There are no files to sync, please check your .gitignore and sync.exclude configuration", diags[0].Summary) } diff --git a/bundle/config/validate/job_cluster_key_defined_test.go b/bundle/config/validate/job_cluster_key_defined_test.go index 176b0fedc..2cbdb7c6a 100644 --- a/bundle/config/validate/job_cluster_key_defined_test.go +++ b/bundle/config/validate/job_cluster_key_defined_test.go @@ -34,7 +34,7 @@ func TestJobClusterKeyDefined(t *testing.T) { } diags := bundle.ApplyReadOnly(context.Background(), bundle.ReadOnly(b), JobClusterKeyDefined()) - require.Len(t, diags, 0) + require.Empty(t, diags) require.NoError(t, diags.Error()) } @@ -59,8 +59,8 @@ func TestJobClusterKeyNotDefined(t *testing.T) { diags := bundle.ApplyReadOnly(context.Background(), bundle.ReadOnly(b), JobClusterKeyDefined()) require.Len(t, diags, 1) require.NoError(t, diags.Error()) - require.Equal(t, diags[0].Severity, diag.Warning) - require.Equal(t, diags[0].Summary, "job_cluster_key do-not-exist is not defined") + require.Equal(t, diag.Warning, diags[0].Severity) + require.Equal(t, "job_cluster_key do-not-exist is not defined", diags[0].Summary) } func TestJobClusterKeyDefinedInDifferentJob(t *testing.T) { @@ -92,6 +92,6 @@ func TestJobClusterKeyDefinedInDifferentJob(t *testing.T) { diags := bundle.ApplyReadOnly(context.Background(), bundle.ReadOnly(b), JobClusterKeyDefined()) require.Len(t, diags, 1) require.NoError(t, diags.Error()) - require.Equal(t, diags[0].Severity, diag.Warning) - require.Equal(t, diags[0].Summary, "job_cluster_key do-not-exist is not defined") + require.Equal(t, diag.Warning, diags[0].Severity) + require.Equal(t, "job_cluster_key do-not-exist is not defined", diags[0].Summary) } diff --git a/bundle/context_test.go b/bundle/context_test.go index 3a0f159d9..89a6df052 100644 --- a/bundle/context_test.go +++ b/bundle/context_test.go @@ -12,7 +12,7 @@ func TestGetPanics(t *testing.T) { defer func() { r := recover() require.NotNil(t, r, "The function did not panic") - assert.Equal(t, r, "context not configured with bundle") + assert.Equal(t, "context not configured with bundle", r) }() Get(context.Background()) diff --git a/bundle/deploy/state_pull_test.go b/bundle/deploy/state_pull_test.go index 36c49fb01..f38b71f6b 100644 --- a/bundle/deploy/state_pull_test.go +++ b/bundle/deploy/state_pull_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "io" "io/fs" "os" @@ -279,7 +278,7 @@ func TestStatePullNoState(t *testing.T) { require.NoError(t, err) _, err = os.Stat(statePath) - require.True(t, errors.Is(err, fs.ErrNotExist)) + require.ErrorIs(t, err, fs.ErrNotExist) } func TestStatePullOlderState(t *testing.T) { diff --git a/bundle/deploy/state_update_test.go b/bundle/deploy/state_update_test.go index e561f534e..04c5579a8 100644 --- a/bundle/deploy/state_update_test.go +++ b/bundle/deploy/state_update_test.go @@ -60,7 +60,7 @@ func TestStateUpdate(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(1), state.Seq) - require.Equal(t, state.Files, Filelist{ + require.Equal(t, Filelist{ { LocalPath: "test1.py", }, @@ -68,7 +68,7 @@ func TestStateUpdate(t *testing.T) { LocalPath: "test2.py", IsNotebook: true, }, - }) + }, state.Files) require.Equal(t, build.GetInfo().Version, state.CliVersion) diags = bundle.Apply(ctx, b, s) @@ -79,7 +79,7 @@ func TestStateUpdate(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(2), state.Seq) - require.Equal(t, state.Files, Filelist{ + require.Equal(t, Filelist{ { LocalPath: "test1.py", }, @@ -87,7 +87,7 @@ func TestStateUpdate(t *testing.T) { LocalPath: "test2.py", IsNotebook: true, }, - }) + }, state.Files) require.Equal(t, build.GetInfo().Version, state.CliVersion) // Valid non-empty UUID is generated. @@ -130,7 +130,7 @@ func TestStateUpdateWithExistingState(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(11), state.Seq) - require.Equal(t, state.Files, Filelist{ + require.Equal(t, Filelist{ { LocalPath: "test1.py", }, @@ -138,7 +138,7 @@ func TestStateUpdateWithExistingState(t *testing.T) { LocalPath: "test2.py", IsNotebook: true, }, - }) + }, state.Files) require.Equal(t, build.GetInfo().Version, state.CliVersion) // Existing UUID is not overwritten. diff --git a/bundle/deploy/terraform/convert_test.go b/bundle/deploy/terraform/convert_test.go index 61f26c088..84b3c5788 100644 --- a/bundle/deploy/terraform/convert_test.go +++ b/bundle/deploy/terraform/convert_test.go @@ -254,10 +254,10 @@ func TestBundleToTerraformPipeline(t *testing.T) { assert.Equal(t, "my pipeline", resource.Name) assert.Len(t, resource.Library, 2) assert.Len(t, resource.Notification, 2) - assert.Equal(t, resource.Notification[0].Alerts, []string{"on-update-fatal-failure"}) - assert.Equal(t, resource.Notification[0].EmailRecipients, []string{"jane@doe.com"}) - assert.Equal(t, resource.Notification[1].Alerts, []string{"on-update-failure", "on-flow-failure"}) - assert.Equal(t, resource.Notification[1].EmailRecipients, []string{"jane@doe.com", "john@doe.com"}) + assert.Equal(t, []string{"on-update-fatal-failure"}, resource.Notification[0].Alerts) + assert.Equal(t, []string{"jane@doe.com"}, resource.Notification[0].EmailRecipients) + assert.Equal(t, []string{"on-update-failure", "on-flow-failure"}, resource.Notification[1].Alerts) + assert.Equal(t, []string{"jane@doe.com", "john@doe.com"}, resource.Notification[1].EmailRecipients) assert.Nil(t, out.Data) } @@ -454,7 +454,7 @@ func TestBundleToTerraformModelServing(t *testing.T) { assert.Equal(t, "name", resource.Name) assert.Equal(t, "model_name", resource.Config.ServedModels[0].ModelName) assert.Equal(t, "1", resource.Config.ServedModels[0].ModelVersion) - assert.Equal(t, true, resource.Config.ServedModels[0].ScaleToZeroEnabled) + assert.True(t, resource.Config.ServedModels[0].ScaleToZeroEnabled) assert.Equal(t, "Small", resource.Config.ServedModels[0].WorkloadSize) assert.Equal(t, "model_name-1", resource.Config.TrafficConfig.Routes[0].ServedModelName) assert.Equal(t, 100, resource.Config.TrafficConfig.Routes[0].TrafficPercentage) diff --git a/bundle/deploy/terraform/init_test.go b/bundle/deploy/terraform/init_test.go index a1ffc5a1a..30ac9e301 100644 --- a/bundle/deploy/terraform/init_test.go +++ b/bundle/deploy/terraform/init_test.go @@ -225,7 +225,7 @@ func TestSetProxyEnvVars(t *testing.T) { env := make(map[string]string, 0) err := setProxyEnvVars(context.Background(), env, b) require.NoError(t, err) - assert.Len(t, env, 0) + assert.Empty(t, env) // Lower case set. clearEnv() @@ -293,7 +293,7 @@ func TestSetUserProfileFromInheritEnvVars(t *testing.T) { require.NoError(t, err) assert.Contains(t, env, "USERPROFILE") - assert.Equal(t, env["USERPROFILE"], "c:\\foo\\c") + assert.Equal(t, "c:\\foo\\c", env["USERPROFILE"]) } func TestInheritEnvVarsWithAbsentTFConfigFile(t *testing.T) { diff --git a/bundle/internal/schema/main_test.go b/bundle/internal/schema/main_test.go index 4eeb41d47..06e89c856 100644 --- a/bundle/internal/schema/main_test.go +++ b/bundle/internal/schema/main_test.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "fmt" "io" "os" "path" @@ -80,7 +79,7 @@ func TestRequiredAnnotationsForNewFields(t *testing.T) { }, }) assert.NoError(t, err) - assert.Empty(t, updatedFieldPaths, fmt.Sprintf("Missing JSON-schema descriptions for new config fields in bundle/internal/schema/annotations.yml:\n%s", strings.Join(updatedFieldPaths, "\n"))) + assert.Empty(t, updatedFieldPaths, "Missing JSON-schema descriptions for new config fields in bundle/internal/schema/annotations.yml:\n%s", strings.Join(updatedFieldPaths, "\n")) } // Checks whether types in annotation files are still present in Config type diff --git a/bundle/libraries/filer_volume_test.go b/bundle/libraries/filer_volume_test.go index 7b2f5c5ba..2af54b0cb 100644 --- a/bundle/libraries/filer_volume_test.go +++ b/bundle/libraries/filer_volume_test.go @@ -212,12 +212,12 @@ func TestFilerForVolumeWithInvalidVolumePaths(t *testing.T) { bundletest.SetLocation(b, "workspace.artifact_path", []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}) _, _, diags := GetFilerForLibraries(context.Background(), b) - require.Equal(t, diags, diag.Diagnostics{{ + require.Equal(t, diag.Diagnostics{{ Severity: diag.Error, Summary: fmt.Sprintf("expected UC volume path to be in the format /Volumes////..., got %s", p), Locations: []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}, Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, - }}) + }}, diags) } } diff --git a/bundle/libraries/helpers_test.go b/bundle/libraries/helpers_test.go index 9d7e12ee5..754aa8f95 100644 --- a/bundle/libraries/helpers_test.go +++ b/bundle/libraries/helpers_test.go @@ -12,25 +12,25 @@ func TestLibraryPath(t *testing.T) { p, err := libraryPath(&compute.Library{Whl: path}) assert.Equal(t, path, p) - assert.Nil(t, err) + assert.NoError(t, err) p, err = libraryPath(&compute.Library{Jar: path}) assert.Equal(t, path, p) - assert.Nil(t, err) + assert.NoError(t, err) p, err = libraryPath(&compute.Library{Egg: path}) assert.Equal(t, path, p) - assert.Nil(t, err) + assert.NoError(t, err) p, err = libraryPath(&compute.Library{Requirements: path}) assert.Equal(t, path, p) - assert.Nil(t, err) + assert.NoError(t, err) p, err = libraryPath(&compute.Library{}) assert.Equal(t, "", p) - assert.NotNil(t, err) + assert.Error(t, err) p, err = libraryPath(&compute.Library{Pypi: &compute.PythonPyPiLibrary{Package: "pypipackage"}}) assert.Equal(t, "", p) - assert.NotNil(t, err) + assert.Error(t, err) } diff --git a/bundle/permissions/filter_test.go b/bundle/permissions/filter_test.go index e6e5a3799..ef7167d75 100644 --- a/bundle/permissions/filter_test.go +++ b/bundle/permissions/filter_test.go @@ -99,32 +99,32 @@ func TestFilterCurrentUser(t *testing.T) { assert.NoError(t, diags.Error()) // Assert current user is filtered out. - assert.Equal(t, 2, len(b.Config.Resources.Jobs["job1"].Permissions)) + assert.Len(t, b.Config.Resources.Jobs["job1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, robot) assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.Jobs["job2"].Permissions)) + assert.Len(t, b.Config.Resources.Jobs["job2"].Permissions, 2) assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, robot) assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.Pipelines["pipeline1"].Permissions)) + assert.Len(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, robot) assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.Experiments["experiment1"].Permissions)) + assert.Len(t, b.Config.Resources.Experiments["experiment1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, robot) assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.Models["model1"].Permissions)) + assert.Len(t, b.Config.Resources.Models["model1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, robot) assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions)) + assert.Len(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, 2) assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, robot) assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, bob) // Assert there's no change to the grant. - assert.Equal(t, 1, len(b.Config.Resources.RegisteredModels["registered_model1"].Grants)) + assert.Len(t, b.Config.Resources.RegisteredModels["registered_model1"].Grants, 1) } func TestFilterCurrentServicePrincipal(t *testing.T) { @@ -134,32 +134,32 @@ func TestFilterCurrentServicePrincipal(t *testing.T) { assert.NoError(t, diags.Error()) // Assert current user is filtered out. - assert.Equal(t, 2, len(b.Config.Resources.Jobs["job1"].Permissions)) + assert.Len(t, b.Config.Resources.Jobs["job1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, alice) assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.Jobs["job2"].Permissions)) + assert.Len(t, b.Config.Resources.Jobs["job2"].Permissions, 2) assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, alice) assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.Pipelines["pipeline1"].Permissions)) + assert.Len(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, alice) assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.Experiments["experiment1"].Permissions)) + assert.Len(t, b.Config.Resources.Experiments["experiment1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, alice) assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.Models["model1"].Permissions)) + assert.Len(t, b.Config.Resources.Models["model1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, alice) assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, bob) - assert.Equal(t, 2, len(b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions)) + assert.Len(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, 2) assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, alice) assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, bob) // Assert there's no change to the grant. - assert.Equal(t, 1, len(b.Config.Resources.RegisteredModels["registered_model1"].Grants)) + assert.Len(t, b.Config.Resources.RegisteredModels["registered_model1"].Grants, 1) } func TestFilterCurrentUserDoesNotErrorWhenNoResources(t *testing.T) { diff --git a/bundle/permissions/mutator_test.go b/bundle/permissions/mutator_test.go index 78703e90f..15586e979 100644 --- a/bundle/permissions/mutator_test.go +++ b/bundle/permissions/mutator_test.go @@ -164,7 +164,7 @@ func TestAllResourcesExplicitlyDefinedForPermissionsSupport(t *testing.T) { for _, resource := range unsupportedResources { _, ok := levelsMap[resource] - assert.False(t, ok, fmt.Sprintf("Resource %s is defined in both levelsMap and unsupportedResources", resource)) + assert.False(t, ok, "Resource %s is defined in both levelsMap and unsupportedResources", resource) } for _, resource := range r.AllResources() { diff --git a/bundle/permissions/permission_diagnostics_test.go b/bundle/permissions/permission_diagnostics_test.go index 7b0afefa0..6c55ab594 100644 --- a/bundle/permissions/permission_diagnostics_test.go +++ b/bundle/permissions/permission_diagnostics_test.go @@ -28,7 +28,7 @@ func TestPermissionDiagnosticsApplyFail(t *testing.T) { }) diags := permissions.PermissionDiagnostics().Apply(context.Background(), b) - require.Equal(t, diags[0].Severity, diag.Warning) + require.Equal(t, diag.Warning, diags[0].Severity) require.Contains(t, diags[0].Summary, "permissions section should include testuser@databricks.com or one of their groups with CAN_MANAGE permissions") } diff --git a/bundle/run/job_test.go b/bundle/run/job_test.go index 5d19ca4ff..72aecc887 100644 --- a/bundle/run/job_test.go +++ b/bundle/run/job_test.go @@ -54,7 +54,7 @@ func TestConvertPythonParams(t *testing.T) { err = runner.convertPythonParams(opts) require.NoError(t, err) require.Contains(t, opts.Job.notebookParams, "__python_params") - require.Equal(t, opts.Job.notebookParams["__python_params"], `["param1","param2","param3"]`) + require.Equal(t, `["param1","param2","param3"]`, opts.Job.notebookParams["__python_params"]) } func TestJobRunnerCancel(t *testing.T) { diff --git a/bundle/tests/complex_variables_test.go b/bundle/tests/complex_variables_test.go index e68823c33..d72b5f157 100644 --- a/bundle/tests/complex_variables_test.go +++ b/bundle/tests/complex_variables_test.go @@ -30,7 +30,7 @@ func TestComplexVariables(t *testing.T) { require.Equal(t, "true", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.SparkConf["spark.speculation"]) require.Equal(t, "true", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.SparkConf["spark.random"]) - require.Equal(t, 3, len(b.Config.Resources.Jobs["my_job"].Tasks[0].Libraries)) + require.Len(t, b.Config.Resources.Jobs["my_job"].Tasks[0].Libraries, 3) require.Contains(t, b.Config.Resources.Jobs["my_job"].Tasks[0].Libraries, compute.Library{ Jar: "/path/to/jar", }) diff --git a/bundle/tests/environment_git_test.go b/bundle/tests/environment_git_test.go index d4695c78d..848b972b1 100644 --- a/bundle/tests/environment_git_test.go +++ b/bundle/tests/environment_git_test.go @@ -2,7 +2,6 @@ package config_tests import ( "context" - "fmt" "strings" "testing" @@ -16,7 +15,7 @@ func TestGitAutoLoadWithEnvironment(t *testing.T) { bundle.Apply(context.Background(), b, mutator.LoadGitDetails()) assert.True(t, b.Config.Bundle.Git.Inferred) validUrl := strings.Contains(b.Config.Bundle.Git.OriginURL, "/cli") || strings.Contains(b.Config.Bundle.Git.OriginURL, "/bricks") - assert.True(t, validUrl, fmt.Sprintf("Expected URL to contain '/cli' or '/bricks', got %s", b.Config.Bundle.Git.OriginURL)) + assert.True(t, validUrl, "Expected URL to contain '/cli' or '/bricks', got %s", b.Config.Bundle.Git.OriginURL) } func TestGitManuallySetBranchWithEnvironment(t *testing.T) { @@ -25,5 +24,5 @@ func TestGitManuallySetBranchWithEnvironment(t *testing.T) { assert.False(t, b.Config.Bundle.Git.Inferred) assert.Equal(t, "main", b.Config.Bundle.Git.Branch) validUrl := strings.Contains(b.Config.Bundle.Git.OriginURL, "/cli") || strings.Contains(b.Config.Bundle.Git.OriginURL, "/bricks") - assert.True(t, validUrl, fmt.Sprintf("Expected URL to contain '/cli' or '/bricks', got %s", b.Config.Bundle.Git.OriginURL)) + assert.True(t, validUrl, "Expected URL to contain '/cli' or '/bricks', got %s", b.Config.Bundle.Git.OriginURL) } diff --git a/bundle/tests/environment_overrides_test.go b/bundle/tests/environment_overrides_test.go index 4a1115048..b68b083ff 100644 --- a/bundle/tests/environment_overrides_test.go +++ b/bundle/tests/environment_overrides_test.go @@ -21,8 +21,8 @@ func TestEnvironmentOverridesResourcesDev(t *testing.T) { assert.Equal(t, "base job", b.Config.Resources.Jobs["job1"].Name) // Base values are preserved in the development environment. - assert.Equal(t, true, b.Config.Resources.Pipelines["boolean1"].Photon) - assert.Equal(t, false, b.Config.Resources.Pipelines["boolean2"].Photon) + assert.True(t, b.Config.Resources.Pipelines["boolean1"].Photon) + assert.False(t, b.Config.Resources.Pipelines["boolean2"].Photon) } func TestEnvironmentOverridesResourcesStaging(t *testing.T) { @@ -30,6 +30,6 @@ func TestEnvironmentOverridesResourcesStaging(t *testing.T) { assert.Equal(t, "staging job", b.Config.Resources.Jobs["job1"].Name) // Override values are applied in the staging environment. - assert.Equal(t, false, b.Config.Resources.Pipelines["boolean1"].Photon) - assert.Equal(t, true, b.Config.Resources.Pipelines["boolean2"].Photon) + assert.False(t, b.Config.Resources.Pipelines["boolean1"].Photon) + assert.True(t, b.Config.Resources.Pipelines["boolean2"].Photon) } diff --git a/bundle/tests/environments_job_and_pipeline_test.go b/bundle/tests/environments_job_and_pipeline_test.go index 218d2e470..423b14c07 100644 --- a/bundle/tests/environments_job_and_pipeline_test.go +++ b/bundle/tests/environments_job_and_pipeline_test.go @@ -10,11 +10,11 @@ import ( func TestJobAndPipelineDevelopmentWithEnvironment(t *testing.T) { b := loadTarget(t, "./environments_job_and_pipeline", "development") - assert.Len(t, b.Config.Resources.Jobs, 0) + assert.Empty(t, b.Config.Resources.Jobs) assert.Len(t, b.Config.Resources.Pipelines, 1) p := b.Config.Resources.Pipelines["nyc_taxi_pipeline"] - assert.Equal(t, b.Config.Bundle.Mode, config.Development) + assert.Equal(t, config.Development, b.Config.Bundle.Mode) assert.True(t, p.Development) require.Len(t, p.Libraries, 1) assert.Equal(t, "./dlt/nyc_taxi_loader", p.Libraries[0].Notebook.Path) @@ -23,7 +23,7 @@ func TestJobAndPipelineDevelopmentWithEnvironment(t *testing.T) { func TestJobAndPipelineStagingWithEnvironment(t *testing.T) { b := loadTarget(t, "./environments_job_and_pipeline", "staging") - assert.Len(t, b.Config.Resources.Jobs, 0) + assert.Empty(t, b.Config.Resources.Jobs) assert.Len(t, b.Config.Resources.Pipelines, 1) p := b.Config.Resources.Pipelines["nyc_taxi_pipeline"] diff --git a/bundle/tests/git_test.go b/bundle/tests/git_test.go index dec6c268a..41293e450 100644 --- a/bundle/tests/git_test.go +++ b/bundle/tests/git_test.go @@ -2,7 +2,6 @@ package config_tests import ( "context" - "fmt" "strings" "testing" @@ -17,7 +16,7 @@ func TestGitAutoLoad(t *testing.T) { bundle.Apply(context.Background(), b, mutator.LoadGitDetails()) assert.True(t, b.Config.Bundle.Git.Inferred) validUrl := strings.Contains(b.Config.Bundle.Git.OriginURL, "/cli") || strings.Contains(b.Config.Bundle.Git.OriginURL, "/bricks") - assert.True(t, validUrl, fmt.Sprintf("Expected URL to contain '/cli' or '/bricks', got %s", b.Config.Bundle.Git.OriginURL)) + assert.True(t, validUrl, "Expected URL to contain '/cli' or '/bricks', got %s", b.Config.Bundle.Git.OriginURL) } func TestGitManuallySetBranch(t *testing.T) { @@ -26,7 +25,7 @@ func TestGitManuallySetBranch(t *testing.T) { assert.False(t, b.Config.Bundle.Git.Inferred) assert.Equal(t, "main", b.Config.Bundle.Git.Branch) validUrl := strings.Contains(b.Config.Bundle.Git.OriginURL, "/cli") || strings.Contains(b.Config.Bundle.Git.OriginURL, "/bricks") - assert.True(t, validUrl, fmt.Sprintf("Expected URL to contain '/cli' or '/bricks', got %s", b.Config.Bundle.Git.OriginURL)) + assert.True(t, validUrl, "Expected URL to contain '/cli' or '/bricks', got %s", b.Config.Bundle.Git.OriginURL) } func TestGitBundleBranchValidation(t *testing.T) { diff --git a/bundle/tests/issue_1828_test.go b/bundle/tests/issue_1828_test.go index 5f2becce5..31fcfeb8e 100644 --- a/bundle/tests/issue_1828_test.go +++ b/bundle/tests/issue_1828_test.go @@ -35,7 +35,7 @@ func TestIssue1828(t *testing.T) { } if assert.Contains(t, b.Config.Variables, "float") { - assert.Equal(t, 3.14, b.Config.Variables["float"].Default) + assert.InDelta(t, 3.14, b.Config.Variables["float"].Default, 0.0001) } if assert.Contains(t, b.Config.Variables, "time") { @@ -43,6 +43,6 @@ func TestIssue1828(t *testing.T) { } if assert.Contains(t, b.Config.Variables, "nil") { - assert.Equal(t, nil, b.Config.Variables["nil"].Default) + assert.Nil(t, b.Config.Variables["nil"].Default) } } diff --git a/bundle/tests/job_and_pipeline_test.go b/bundle/tests/job_and_pipeline_test.go index 65aa5bdc4..408e3e3ef 100644 --- a/bundle/tests/job_and_pipeline_test.go +++ b/bundle/tests/job_and_pipeline_test.go @@ -10,11 +10,11 @@ import ( func TestJobAndPipelineDevelopment(t *testing.T) { b := loadTarget(t, "./job_and_pipeline", "development") - assert.Len(t, b.Config.Resources.Jobs, 0) + assert.Empty(t, b.Config.Resources.Jobs) assert.Len(t, b.Config.Resources.Pipelines, 1) p := b.Config.Resources.Pipelines["nyc_taxi_pipeline"] - assert.Equal(t, b.Config.Bundle.Mode, config.Development) + assert.Equal(t, config.Development, b.Config.Bundle.Mode) assert.True(t, p.Development) require.Len(t, p.Libraries, 1) assert.Equal(t, "./dlt/nyc_taxi_loader", p.Libraries[0].Notebook.Path) @@ -23,7 +23,7 @@ func TestJobAndPipelineDevelopment(t *testing.T) { func TestJobAndPipelineStaging(t *testing.T) { b := loadTarget(t, "./job_and_pipeline", "staging") - assert.Len(t, b.Config.Resources.Jobs, 0) + assert.Empty(t, b.Config.Resources.Jobs) assert.Len(t, b.Config.Resources.Pipelines, 1) p := b.Config.Resources.Pipelines["nyc_taxi_pipeline"] diff --git a/bundle/tests/job_cluster_key_test.go b/bundle/tests/job_cluster_key_test.go index 5a8b368e5..6a08da89c 100644 --- a/bundle/tests/job_cluster_key_test.go +++ b/bundle/tests/job_cluster_key_test.go @@ -16,13 +16,13 @@ func TestJobClusterKeyNotDefinedTest(t *testing.T) { diags := bundle.ApplyReadOnly(context.Background(), bundle.ReadOnly(b), validate.JobClusterKeyDefined()) require.Len(t, diags, 1) require.NoError(t, diags.Error()) - require.Equal(t, diags[0].Severity, diag.Warning) - require.Equal(t, diags[0].Summary, "job_cluster_key key is not defined") + require.Equal(t, diag.Warning, diags[0].Severity) + require.Equal(t, "job_cluster_key key is not defined", diags[0].Summary) } func TestJobClusterKeyDefinedTest(t *testing.T) { b := loadTarget(t, "./job_cluster_key", "development") diags := bundle.ApplyReadOnly(context.Background(), bundle.ReadOnly(b), validate.JobClusterKeyDefined()) - require.Len(t, diags, 0) + require.Empty(t, diags) } diff --git a/bundle/tests/model_serving_endpoint_test.go b/bundle/tests/model_serving_endpoint_test.go index b8b800863..f779a07e6 100644 --- a/bundle/tests/model_serving_endpoint_test.go +++ b/bundle/tests/model_serving_endpoint_test.go @@ -20,7 +20,7 @@ func assertExpected(t *testing.T, p *resources.ModelServingEndpoint) { func TestModelServingEndpointDevelopment(t *testing.T) { b := loadTarget(t, "./model_serving_endpoint", "development") assert.Len(t, b.Config.Resources.ModelServingEndpoints, 1) - assert.Equal(t, b.Config.Bundle.Mode, config.Development) + assert.Equal(t, config.Development, b.Config.Bundle.Mode) p := b.Config.Resources.ModelServingEndpoints["my_model_serving_endpoint"] assert.Equal(t, "my-dev-endpoint", p.Name) diff --git a/bundle/tests/override_job_tasks_test.go b/bundle/tests/override_job_tasks_test.go index 82da04da2..85463e17c 100644 --- a/bundle/tests/override_job_tasks_test.go +++ b/bundle/tests/override_job_tasks_test.go @@ -12,14 +12,14 @@ func TestOverrideTasksDev(t *testing.T) { assert.Len(t, b.Config.Resources.Jobs["foo"].Tasks, 2) tasks := b.Config.Resources.Jobs["foo"].Tasks - assert.Equal(t, tasks[0].TaskKey, "key1") - assert.Equal(t, tasks[0].NewCluster.NodeTypeId, "i3.xlarge") - assert.Equal(t, tasks[0].NewCluster.NumWorkers, 1) - assert.Equal(t, tasks[0].SparkPythonTask.PythonFile, "./test1.py") + assert.Equal(t, "key1", tasks[0].TaskKey) + assert.Equal(t, "i3.xlarge", tasks[0].NewCluster.NodeTypeId) + assert.Equal(t, 1, tasks[0].NewCluster.NumWorkers) + assert.Equal(t, "./test1.py", tasks[0].SparkPythonTask.PythonFile) - assert.Equal(t, tasks[1].TaskKey, "key2") - assert.Equal(t, tasks[1].NewCluster.SparkVersion, "13.3.x-scala2.12") - assert.Equal(t, tasks[1].SparkPythonTask.PythonFile, "./test2.py") + assert.Equal(t, "key2", tasks[1].TaskKey) + assert.Equal(t, "13.3.x-scala2.12", tasks[1].NewCluster.SparkVersion) + assert.Equal(t, "./test2.py", tasks[1].SparkPythonTask.PythonFile) } func TestOverrideTasksStaging(t *testing.T) { @@ -28,12 +28,12 @@ func TestOverrideTasksStaging(t *testing.T) { assert.Len(t, b.Config.Resources.Jobs["foo"].Tasks, 2) tasks := b.Config.Resources.Jobs["foo"].Tasks - assert.Equal(t, tasks[0].TaskKey, "key1") - assert.Equal(t, tasks[0].NewCluster.SparkVersion, "13.3.x-scala2.12") - assert.Equal(t, tasks[0].SparkPythonTask.PythonFile, "./test1.py") + assert.Equal(t, "key1", tasks[0].TaskKey) + assert.Equal(t, "13.3.x-scala2.12", tasks[0].NewCluster.SparkVersion) + assert.Equal(t, "./test1.py", tasks[0].SparkPythonTask.PythonFile) - assert.Equal(t, tasks[1].TaskKey, "key2") - assert.Equal(t, tasks[1].NewCluster.NodeTypeId, "i3.2xlarge") - assert.Equal(t, tasks[1].NewCluster.NumWorkers, 4) - assert.Equal(t, tasks[1].SparkPythonTask.PythonFile, "./test3.py") + assert.Equal(t, "key2", tasks[1].TaskKey) + assert.Equal(t, "i3.2xlarge", tasks[1].NewCluster.NodeTypeId) + assert.Equal(t, 4, tasks[1].NewCluster.NumWorkers) + assert.Equal(t, "./test3.py", tasks[1].SparkPythonTask.PythonFile) } diff --git a/bundle/tests/presets_test.go b/bundle/tests/presets_test.go index 5fcb5d95b..c2cbe497b 100644 --- a/bundle/tests/presets_test.go +++ b/bundle/tests/presets_test.go @@ -13,7 +13,7 @@ func TestPresetsDev(t *testing.T) { assert.Equal(t, "myprefix", b.Config.Presets.NamePrefix) assert.Equal(t, config.Paused, b.Config.Presets.TriggerPauseStatus) assert.Equal(t, 10, b.Config.Presets.JobsMaxConcurrentRuns) - assert.Equal(t, true, *b.Config.Presets.PipelinesDevelopment) + assert.True(t, *b.Config.Presets.PipelinesDevelopment) assert.Equal(t, "true", b.Config.Presets.Tags["dev"]) assert.Equal(t, "finance", b.Config.Presets.Tags["team"]) assert.Equal(t, "false", b.Config.Presets.Tags["prod"]) @@ -22,7 +22,7 @@ func TestPresetsDev(t *testing.T) { func TestPresetsProd(t *testing.T) { b := loadTarget(t, "./presets", "prod") - assert.Equal(t, false, *b.Config.Presets.PipelinesDevelopment) + assert.False(t, *b.Config.Presets.PipelinesDevelopment) assert.Equal(t, "finance", b.Config.Presets.Tags["team"]) assert.Equal(t, "true", b.Config.Presets.Tags["prod"]) } diff --git a/bundle/tests/python_wheel_test.go b/bundle/tests/python_wheel_test.go index c982c09d6..06cb05270 100644 --- a/bundle/tests/python_wheel_test.go +++ b/bundle/tests/python_wheel_test.go @@ -23,7 +23,7 @@ func TestPythonWheelBuild(t *testing.T) { matches, err := filepath.Glob("./python_wheel/python_wheel/my_test_code/dist/my_test_code-*.whl") require.NoError(t, err) - require.Equal(t, 1, len(matches)) + require.Len(t, matches, 1) match := libraries.ExpandGlobReferences() diags = bundle.Apply(ctx, b, match) @@ -39,7 +39,7 @@ func TestPythonWheelBuildAutoDetect(t *testing.T) { matches, err := filepath.Glob("./python_wheel/python_wheel_no_artifact/dist/my_test_code-*.whl") require.NoError(t, err) - require.Equal(t, 1, len(matches)) + require.Len(t, matches, 1) match := libraries.ExpandGlobReferences() diags = bundle.Apply(ctx, b, match) @@ -55,7 +55,7 @@ func TestPythonWheelBuildAutoDetectWithNotebookTask(t *testing.T) { matches, err := filepath.Glob("./python_wheel/python_wheel_no_artifact_notebook/dist/my_test_code-*.whl") require.NoError(t, err) - require.Equal(t, 1, len(matches)) + require.Len(t, matches, 1) match := libraries.ExpandGlobReferences() diags = bundle.Apply(ctx, b, match) @@ -108,7 +108,7 @@ func TestPythonWheelBuildWithEnvironmentKey(t *testing.T) { matches, err := filepath.Glob("./python_wheel/environment_key/my_test_code/dist/my_test_code-*.whl") require.NoError(t, err) - require.Equal(t, 1, len(matches)) + require.Len(t, matches, 1) match := libraries.ExpandGlobReferences() diags = bundle.Apply(ctx, b, match) @@ -124,7 +124,7 @@ func TestPythonWheelBuildMultiple(t *testing.T) { matches, err := filepath.Glob("./python_wheel/python_wheel_multiple/my_test_code/dist/my_test_code*.whl") require.NoError(t, err) - require.Equal(t, 2, len(matches)) + require.Len(t, matches, 2) match := libraries.ExpandGlobReferences() diags = bundle.Apply(ctx, b, match) diff --git a/bundle/tests/quality_monitor_test.go b/bundle/tests/quality_monitor_test.go index 9b91052f5..e95c7b7c1 100644 --- a/bundle/tests/quality_monitor_test.go +++ b/bundle/tests/quality_monitor_test.go @@ -19,7 +19,7 @@ func assertExpectedMonitor(t *testing.T, p *resources.QualityMonitor) { func TestMonitorTableNames(t *testing.T) { b := loadTarget(t, "./quality_monitor", "development") assert.Len(t, b.Config.Resources.QualityMonitors, 1) - assert.Equal(t, b.Config.Bundle.Mode, config.Development) + assert.Equal(t, config.Development, b.Config.Bundle.Mode) p := b.Config.Resources.QualityMonitors["my_monitor"] assert.Equal(t, "main.test.dev", p.TableName) diff --git a/bundle/tests/registered_model_test.go b/bundle/tests/registered_model_test.go index 008db8bdd..e9d572a3a 100644 --- a/bundle/tests/registered_model_test.go +++ b/bundle/tests/registered_model_test.go @@ -19,7 +19,7 @@ func assertExpectedModel(t *testing.T, p *resources.RegisteredModel) { func TestRegisteredModelDevelopment(t *testing.T) { b := loadTarget(t, "./registered_model", "development") assert.Len(t, b.Config.Resources.RegisteredModels, 1) - assert.Equal(t, b.Config.Bundle.Mode, config.Development) + assert.Equal(t, config.Development, b.Config.Bundle.Mode) p := b.Config.Resources.RegisteredModels["my_registered_model"] assert.Equal(t, "my-dev-model", p.Name) diff --git a/bundle/tests/sync_include_exclude_no_matches_test.go b/bundle/tests/sync_include_exclude_no_matches_test.go index 0192b61e6..c206e7471 100644 --- a/bundle/tests/sync_include_exclude_no_matches_test.go +++ b/bundle/tests/sync_include_exclude_no_matches_test.go @@ -20,26 +20,26 @@ func TestSyncIncludeExcludeNoMatchesTest(t *testing.T) { require.Len(t, diags, 3) require.NoError(t, diags.Error()) - require.Equal(t, diags[0].Severity, diag.Warning) - require.Equal(t, diags[0].Summary, "Pattern dist does not match any files") + require.Equal(t, diag.Warning, diags[0].Severity) + require.Equal(t, "Pattern dist does not match any files", diags[0].Summary) require.Len(t, diags[0].Paths, 1) - require.Equal(t, diags[0].Paths[0].String(), "sync.exclude[0]") + require.Equal(t, "sync.exclude[0]", diags[0].Paths[0].String()) assert.Len(t, diags[0].Locations, 1) require.Equal(t, diags[0].Locations[0].File, filepath.Join("sync", "override", "databricks.yml")) - require.Equal(t, diags[0].Locations[0].Line, 17) - require.Equal(t, diags[0].Locations[0].Column, 11) + require.Equal(t, 17, diags[0].Locations[0].Line) + require.Equal(t, 11, diags[0].Locations[0].Column) summaries := []string{ fmt.Sprintf("Pattern %s does not match any files", filepath.Join("src", "*")), fmt.Sprintf("Pattern %s does not match any files", filepath.Join("tests", "*")), } - require.Equal(t, diags[1].Severity, diag.Warning) + require.Equal(t, diag.Warning, diags[1].Severity) require.Contains(t, summaries, diags[1].Summary) - require.Equal(t, diags[2].Severity, diag.Warning) + require.Equal(t, diag.Warning, diags[2].Severity) require.Contains(t, summaries, diags[2].Summary) } @@ -47,7 +47,7 @@ func TestSyncIncludeWithNegate(t *testing.T) { b := loadTarget(t, "./sync/negate", "default") diags := bundle.ApplyReadOnly(context.Background(), bundle.ReadOnly(b), validate.ValidateSyncPatterns()) - require.Len(t, diags, 0) + require.Empty(t, diags) require.NoError(t, diags.Error()) } @@ -58,6 +58,6 @@ func TestSyncIncludeWithNegateNoMatches(t *testing.T) { require.Len(t, diags, 1) require.NoError(t, diags.Error()) - require.Equal(t, diags[0].Severity, diag.Warning) - require.Equal(t, diags[0].Summary, "Pattern !*.txt2 does not match any files") + require.Equal(t, diag.Warning, diags[0].Severity) + require.Equal(t, "Pattern !*.txt2 does not match any files", diags[0].Summary) } diff --git a/bundle/tests/sync_test.go b/bundle/tests/sync_test.go index 15644b67e..f5a0296a9 100644 --- a/bundle/tests/sync_test.go +++ b/bundle/tests/sync_test.go @@ -115,12 +115,12 @@ func TestSyncPathsNoRoot(t *testing.T) { // If set to nil, it won't sync anything. b = loadTarget(t, "./sync/paths_no_root", "nil") assert.Equal(t, filepath.FromSlash("sync/paths_no_root"), b.SyncRootPath) - assert.Len(t, b.Config.Sync.Paths, 0) + assert.Empty(t, b.Config.Sync.Paths) // If set to an empty sequence, it won't sync anything. b = loadTarget(t, "./sync/paths_no_root", "empty") assert.Equal(t, filepath.FromSlash("sync/paths_no_root"), b.SyncRootPath) - assert.Len(t, b.Config.Sync.Paths, 0) + assert.Empty(t, b.Config.Sync.Paths) } func TestSyncSharedCode(t *testing.T) { diff --git a/cmd/bundle/generate/dashboard_test.go b/cmd/bundle/generate/dashboard_test.go index f1161950b..33a463ea0 100644 --- a/cmd/bundle/generate/dashboard_test.go +++ b/cmd/bundle/generate/dashboard_test.go @@ -44,7 +44,7 @@ func TestDashboard_ErrorOnLegacyDashboard(t *testing.T) { _, diags := d.resolveID(ctx, b) require.Len(t, diags, 1) - assert.Equal(t, diags[0].Summary, "dashboard \"legacy dashboard\" is a legacy dashboard") + assert.Equal(t, "dashboard \"legacy dashboard\" is a legacy dashboard", diags[0].Summary) } func TestDashboard_ExistingID_Nominal(t *testing.T) { diff --git a/cmd/bundle/generate/generate_test.go b/cmd/bundle/generate/generate_test.go index 896b7de51..395d4ebd4 100644 --- a/cmd/bundle/generate/generate_test.go +++ b/cmd/bundle/generate/generate_test.go @@ -3,7 +3,6 @@ package generate import ( "bytes" "context" - "errors" "fmt" "io" "io/fs" @@ -302,7 +301,7 @@ func TestGenerateJobCommandOldFileRename(t *testing.T) { // Make sure file do not exists after the run _, err = os.Stat(oldFilename) - require.True(t, errors.Is(err, fs.ErrNotExist)) + require.ErrorIs(t, err, fs.ErrNotExist) data, err := os.ReadFile(filepath.Join(configDir, "test_job.job.yml")) require.NoError(t, err) diff --git a/cmd/configure/configure_test.go b/cmd/configure/configure_test.go index e2f6c1e29..14eb0674a 100644 --- a/cmd/configure/configure_test.go +++ b/cmd/configure/configure_test.go @@ -148,9 +148,9 @@ func TestEnvVarsConfigureNoInteractive(t *testing.T) { // We should only save host and token for a profile, other env variables should not be saved _, err = defaultSection.GetKey("auth_type") - assert.NotNil(t, err) + assert.Error(t, err) _, err = defaultSection.GetKey("metadata_service_url") - assert.NotNil(t, err) + assert.Error(t, err) } func TestEnvVarsConfigureNoArgsNoInteractive(t *testing.T) { diff --git a/cmd/labs/github/ref_test.go b/cmd/labs/github/ref_test.go index cc27d1e81..9668cd7ec 100644 --- a/cmd/labs/github/ref_test.go +++ b/cmd/labs/github/ref_test.go @@ -7,14 +7,15 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestFileFromRef(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/databrickslabs/ucx/main/README.md" { _, err := w.Write([]byte(`abc`)) - require.NoError(t, err) + if !assert.NoError(t, err) { + return + } return } t.Logf("Requested: %s", r.URL.Path) @@ -34,7 +35,9 @@ func TestDownloadZipball(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/repos/databrickslabs/ucx/zipball/main" { _, err := w.Write([]byte(`abc`)) - require.NoError(t, err) + if !assert.NoError(t, err) { + return + } return } t.Logf("Requested: %s", r.URL.Path) diff --git a/cmd/labs/github/releases_test.go b/cmd/labs/github/releases_test.go index 9c3d7a959..93ac33aee 100644 --- a/cmd/labs/github/releases_test.go +++ b/cmd/labs/github/releases_test.go @@ -7,14 +7,15 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestLoadsReleasesForCLI(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/repos/databricks/cli/releases" { _, err := w.Write([]byte(`[{"tag_name": "v1.2.3"}, {"tag_name": "v1.2.2"}]`)) - require.NoError(t, err) + if !assert.NoError(t, err) { + return + } return } t.Logf("Requested: %s", r.URL.Path) diff --git a/cmd/labs/github/repositories_test.go b/cmd/labs/github/repositories_test.go index 412b440bc..29ec2ce03 100644 --- a/cmd/labs/github/repositories_test.go +++ b/cmd/labs/github/repositories_test.go @@ -7,14 +7,13 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestRepositories(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/users/databrickslabs/repos" { _, err := w.Write([]byte(`[{"name": "x"}]`)) - require.NoError(t, err) + assert.NoError(t, err) return } t.Logf("Requested: %s", r.URL.Path) @@ -28,5 +27,5 @@ func TestRepositories(t *testing.T) { r := NewRepositoryCache("databrickslabs", t.TempDir()) all, err := r.Load(ctx) assert.NoError(t, err) - assert.True(t, len(all) > 0) + assert.NotEmpty(t, all) } diff --git a/cmd/labs/localcache/jsonfile_test.go b/cmd/labs/localcache/jsonfile_test.go index 0d852174c..9d42c6179 100644 --- a/cmd/labs/localcache/jsonfile_test.go +++ b/cmd/labs/localcache/jsonfile_test.go @@ -22,7 +22,7 @@ func TestCreatesDirectoryIfNeeded(t *testing.T) { } first, err := c.Load(ctx, tick) assert.NoError(t, err) - assert.Equal(t, first, int64(1)) + assert.Equal(t, int64(1), first) } func TestImpossibleToCreateDir(t *testing.T) { diff --git a/cmd/labs/project/installer_test.go b/cmd/labs/project/installer_test.go index a69389b31..a01ba864a 100644 --- a/cmd/labs/project/installer_test.go +++ b/cmd/labs/project/installer_test.go @@ -26,6 +26,7 @@ import ( "github.com/databricks/databricks-sdk-go/service/compute" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/sql" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -169,17 +170,17 @@ func TestInstallerWorksForReleases(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/databrickslabs/blueprint/v0.3.15/labs.yml" { raw, err := os.ReadFile("testdata/installed-in-home/.databricks/labs/blueprint/lib/labs.yml") - require.NoError(t, err) + assert.NoError(t, err) _, err = w.Write(raw) - require.NoError(t, err) + assert.NoError(t, err) return } if r.URL.Path == "/repos/databrickslabs/blueprint/zipball/v0.3.15" { raw, err := zipballFromFolder("testdata/installed-in-home/.databricks/labs/blueprint/lib") - require.NoError(t, err) + assert.NoError(t, err) w.Header().Add("Content-Type", "application/octet-stream") _, err = w.Write(raw) - require.NoError(t, err) + assert.NoError(t, err) return } if r.URL.Path == "/api/2.1/clusters/get" { @@ -376,17 +377,17 @@ func TestUpgraderWorksForReleases(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/databrickslabs/blueprint/v0.4.0/labs.yml" { raw, err := os.ReadFile("testdata/installed-in-home/.databricks/labs/blueprint/lib/labs.yml") - require.NoError(t, err) + assert.NoError(t, err) _, err = w.Write(raw) - require.NoError(t, err) + assert.NoError(t, err) return } if r.URL.Path == "/repos/databrickslabs/blueprint/zipball/v0.4.0" { raw, err := zipballFromFolder("testdata/installed-in-home/.databricks/labs/blueprint/lib") - require.NoError(t, err) + assert.NoError(t, err) w.Header().Add("Content-Type", "application/octet-stream") _, err = w.Write(raw) - require.NoError(t, err) + assert.NoError(t, err) return } if r.URL.Path == "/api/2.1/clusters/get" { diff --git a/integration/bundle/artifacts_test.go b/integration/bundle/artifacts_test.go index 1b71a1c3d..9dafe2f83 100644 --- a/integration/bundle/artifacts_test.go +++ b/integration/bundle/artifacts_test.go @@ -85,13 +85,13 @@ func TestUploadArtifactFileToCorrectRemotePath(t *testing.T) { // The remote path attribute on the artifact file should have been set. require.Regexp(t, - regexp.MustCompile(path.Join(regexp.QuoteMeta(wsDir), `.internal/test\.whl`)), + path.Join(regexp.QuoteMeta(wsDir), `.internal/test\.whl`), b.Config.Artifacts["test"].Files[0].RemotePath, ) // The task library path should have been updated to the remote path. require.Regexp(t, - regexp.MustCompile(path.Join("/Workspace", regexp.QuoteMeta(wsDir), `.internal/test\.whl`)), + path.Join("/Workspace", regexp.QuoteMeta(wsDir), `.internal/test\.whl`), b.Config.Resources.Jobs["test"].JobSettings.Tasks[0].Libraries[0].Whl, ) } @@ -149,13 +149,13 @@ func TestUploadArtifactFileToCorrectRemotePathWithEnvironments(t *testing.T) { // The remote path attribute on the artifact file should have been set. require.Regexp(t, - regexp.MustCompile(path.Join(regexp.QuoteMeta(wsDir), `.internal/test\.whl`)), + path.Join(regexp.QuoteMeta(wsDir), `.internal/test\.whl`), b.Config.Artifacts["test"].Files[0].RemotePath, ) // The job environment deps path should have been updated to the remote path. require.Regexp(t, - regexp.MustCompile(path.Join("/Workspace", regexp.QuoteMeta(wsDir), `.internal/test\.whl`)), + path.Join("/Workspace", regexp.QuoteMeta(wsDir), `.internal/test\.whl`), b.Config.Resources.Jobs["test"].JobSettings.Environments[0].Spec.Dependencies[0], ) } @@ -218,13 +218,13 @@ func TestUploadArtifactFileToCorrectRemotePathForVolumes(t *testing.T) { // The remote path attribute on the artifact file should have been set. require.Regexp(t, - regexp.MustCompile(path.Join(regexp.QuoteMeta(volumePath), `.internal/test\.whl`)), + path.Join(regexp.QuoteMeta(volumePath), `.internal/test\.whl`), b.Config.Artifacts["test"].Files[0].RemotePath, ) // The task library path should have been updated to the remote path. require.Regexp(t, - regexp.MustCompile(path.Join(regexp.QuoteMeta(volumePath), `.internal/test\.whl`)), + path.Join(regexp.QuoteMeta(volumePath), `.internal/test\.whl`), b.Config.Resources.Jobs["test"].JobSettings.Tasks[0].Libraries[0].Whl, ) } diff --git a/integration/bundle/deploy_test.go b/integration/bundle/deploy_test.go index 0b37e5630..309b82917 100644 --- a/integration/bundle/deploy_test.go +++ b/integration/bundle/deploy_test.go @@ -2,7 +2,6 @@ package bundle_test import ( "context" - "errors" "fmt" "io" "os" @@ -99,7 +98,7 @@ func TestBundleDeployUcSchema(t *testing.T) { // Assert the schema is deleted _, err = w.Schemas.GetByFullName(ctx, strings.Join([]string{catalogName, schemaName}, ".")) apiErr := &apierr.APIError{} - assert.True(t, errors.As(err, &apiErr)) + assert.ErrorAs(t, err, &apiErr) assert.Equal(t, "SCHEMA_DOES_NOT_EXIST", apiErr.ErrorCode) } diff --git a/integration/bundle/destroy_test.go b/integration/bundle/destroy_test.go index f18138ce5..b69382a58 100644 --- a/integration/bundle/destroy_test.go +++ b/integration/bundle/destroy_test.go @@ -1,7 +1,6 @@ package bundle_test import ( - "errors" "os" "path/filepath" "testing" @@ -71,11 +70,11 @@ func TestBundleDestroy(t *testing.T) { // Assert snapshot file is deleted entries, err = os.ReadDir(snapshotsDir) require.NoError(t, err) - assert.Len(t, entries, 0) + assert.Empty(t, entries) // Assert bundle deployment path is deleted _, err = w.Workspace.GetStatusByPath(ctx, remoteRoot) apiErr := &apierr.APIError{} - assert.True(t, errors.As(err, &apiErr)) + assert.ErrorAs(t, err, &apiErr) assert.Equal(t, "RESOURCE_DOES_NOT_EXIST", apiErr.ErrorCode) } diff --git a/integration/cmd/fs/ls_test.go b/integration/cmd/fs/ls_test.go index 58e776d8a..25929fdf3 100644 --- a/integration/cmd/fs/ls_test.go +++ b/integration/cmd/fs/ls_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "io/fs" "path" - "regexp" "strings" "testing" @@ -65,11 +64,11 @@ func TestFsLs(t *testing.T) { assert.Equal(t, "a", parsedStdout[0]["name"]) assert.Equal(t, true, parsedStdout[0]["is_directory"]) - assert.Equal(t, float64(0), parsedStdout[0]["size"]) + assert.InDelta(t, float64(0), parsedStdout[0]["size"], 0.0001) assert.Equal(t, "bye.txt", parsedStdout[1]["name"]) assert.Equal(t, false, parsedStdout[1]["is_directory"]) - assert.Equal(t, float64(3), parsedStdout[1]["size"]) + assert.InDelta(t, float64(3), parsedStdout[1]["size"], 0.0001) }) } } @@ -99,11 +98,11 @@ func TestFsLsWithAbsolutePaths(t *testing.T) { assert.Equal(t, path.Join(tmpDir, "a"), parsedStdout[0]["name"]) assert.Equal(t, true, parsedStdout[0]["is_directory"]) - assert.Equal(t, float64(0), parsedStdout[0]["size"]) + assert.InDelta(t, float64(0), parsedStdout[0]["size"], 0.0001) assert.Equal(t, path.Join(tmpDir, "bye.txt"), parsedStdout[1]["name"]) assert.Equal(t, false, parsedStdout[1]["is_directory"]) - assert.Equal(t, float64(3), parsedStdout[1]["size"]) + assert.InDelta(t, float64(3), parsedStdout[1]["size"].(float64), 0.0001) }) } } @@ -122,7 +121,7 @@ func TestFsLsOnFile(t *testing.T) { setupLsFiles(t, f) _, _, err := testcli.RequireErrorRun(t, ctx, "fs", "ls", path.Join(tmpDir, "a", "hello.txt"), "--output=json") - assert.Regexp(t, regexp.MustCompile("not a directory: .*/a/hello.txt"), err.Error()) + assert.Regexp(t, "not a directory: .*/a/hello.txt", err.Error()) assert.ErrorAs(t, err, &filer.NotADirectory{}) }) } @@ -147,7 +146,7 @@ func TestFsLsOnEmptyDir(t *testing.T) { require.NoError(t, err) // assert on ls output - assert.Equal(t, 0, len(parsedStdout)) + assert.Empty(t, parsedStdout) }) } } @@ -166,7 +165,7 @@ func TestFsLsForNonexistingDir(t *testing.T) { _, _, err := testcli.RequireErrorRun(t, ctx, "fs", "ls", path.Join(tmpDir, "nonexistent"), "--output=json") assert.ErrorIs(t, err, fs.ErrNotExist) - assert.Regexp(t, regexp.MustCompile("no such directory: .*/nonexistent"), err.Error()) + assert.Regexp(t, "no such directory: .*/nonexistent", err.Error()) }) } } diff --git a/integration/cmd/fs/mkdir_test.go b/integration/cmd/fs/mkdir_test.go index f332bb526..eff0599a7 100644 --- a/integration/cmd/fs/mkdir_test.go +++ b/integration/cmd/fs/mkdir_test.go @@ -34,7 +34,7 @@ func TestFsMkdir(t *testing.T) { info, err := f.Stat(context.Background(), "a") require.NoError(t, err) assert.Equal(t, "a", info.Name()) - assert.Equal(t, true, info.IsDir()) + assert.True(t, info.IsDir()) }) } } @@ -60,19 +60,19 @@ func TestFsMkdirCreatesIntermediateDirectories(t *testing.T) { infoA, err := f.Stat(context.Background(), "a") require.NoError(t, err) assert.Equal(t, "a", infoA.Name()) - assert.Equal(t, true, infoA.IsDir()) + assert.True(t, infoA.IsDir()) // assert directory "b" is created infoB, err := f.Stat(context.Background(), "a/b") require.NoError(t, err) assert.Equal(t, "b", infoB.Name()) - assert.Equal(t, true, infoB.IsDir()) + assert.True(t, infoB.IsDir()) // assert directory "c" is created infoC, err := f.Stat(context.Background(), "a/b/c") require.NoError(t, err) assert.Equal(t, "c", infoC.Name()) - assert.Equal(t, true, infoC.IsDir()) + assert.True(t, infoC.IsDir()) }) } } diff --git a/integration/cmd/sync/sync_test.go b/integration/cmd/sync/sync_test.go index 984f6ac49..6f58b7e42 100644 --- a/integration/cmd/sync/sync_test.go +++ b/integration/cmd/sync/sync_test.go @@ -225,7 +225,7 @@ func (a *syncTest) snapshotContains(files []string) { assert.Equal(a.t, s.RemotePath, a.remoteRoot) for _, filePath := range files { _, ok := s.LastModifiedTimes[filePath] - assert.True(a.t, ok, fmt.Sprintf("%s not in snapshot file: %v", filePath, s.LastModifiedTimes)) + assert.True(a.t, ok, "%s not in snapshot file: %v", filePath, s.LastModifiedTimes) } assert.Equal(a.t, len(files), len(s.LastModifiedTimes)) } diff --git a/integration/libs/filer/filer_test.go b/integration/libs/filer/filer_test.go index 766f9817b..21c839e1b 100644 --- a/integration/libs/filer/filer_test.go +++ b/integration/libs/filer/filer_test.go @@ -4,11 +4,9 @@ import ( "bytes" "context" "encoding/json" - "errors" "io" "io/fs" "path" - "regexp" "strings" "testing" @@ -106,7 +104,7 @@ func commonFilerRecursiveDeleteTest(t *testing.T, ctx context.Context, f filer.F for _, e := range entriesBeforeDelete { names = append(names, e.Name()) } - assert.Equal(t, names, []string{"file1", "file2", "subdir1", "subdir2"}) + assert.Equal(t, []string{"file1", "file2", "subdir1", "subdir2"}, names) err = f.Delete(ctx, "dir") assert.ErrorAs(t, err, &filer.DirectoryNotEmptyError{}) @@ -149,13 +147,13 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // Write should fail because the intermediate directory doesn't exist. err = f.Write(ctx, "/foo/bar", strings.NewReader(`hello world`)) - assert.True(t, errors.As(err, &filer.NoSuchDirectoryError{})) - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorAs(t, err, &filer.NoSuchDirectoryError{}) + assert.ErrorIs(t, err, fs.ErrNotExist) // Read should fail because the intermediate directory doesn't yet exist. _, err = f.Read(ctx, "/foo/bar") - assert.True(t, errors.As(err, &filer.FileDoesNotExistError{})) - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorAs(t, err, &filer.FileDoesNotExistError{}) + assert.ErrorIs(t, err, fs.ErrNotExist) // Read should fail because the path points to a directory err = f.Mkdir(ctx, "/dir") @@ -170,8 +168,8 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // Write should fail because there is an existing file at the specified path. err = f.Write(ctx, "/foo/bar", strings.NewReader(`hello universe`)) - assert.True(t, errors.As(err, &filer.FileAlreadyExistsError{})) - assert.True(t, errors.Is(err, fs.ErrExist)) + assert.ErrorAs(t, err, &filer.FileAlreadyExistsError{}) + assert.ErrorIs(t, err, fs.ErrExist) // Write with OverwriteIfExists should succeed. err = f.Write(ctx, "/foo/bar", strings.NewReader(`hello universe`), filer.OverwriteIfExists) @@ -188,7 +186,7 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) require.NoError(t, err) assert.Equal(t, "foo", info.Name()) assert.True(t, info.Mode().IsDir()) - assert.Equal(t, true, info.IsDir()) + assert.True(t, info.IsDir()) // Stat on a file should succeed. // Note: size and modification time behave differently between backends. @@ -196,17 +194,17 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) require.NoError(t, err) assert.Equal(t, "bar", info.Name()) assert.True(t, info.Mode().IsRegular()) - assert.Equal(t, false, info.IsDir()) + assert.False(t, info.IsDir()) // Delete should fail if the file doesn't exist. err = f.Delete(ctx, "/doesnt_exist") assert.ErrorAs(t, err, &filer.FileDoesNotExistError{}) - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) // Stat should fail if the file doesn't exist. _, err = f.Stat(ctx, "/doesnt_exist") assert.ErrorAs(t, err, &filer.FileDoesNotExistError{}) - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) // Delete should succeed for file that does exist. err = f.Delete(ctx, "/foo/bar") @@ -215,7 +213,7 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // Delete should fail for a non-empty directory. err = f.Delete(ctx, "/foo") assert.ErrorAs(t, err, &filer.DirectoryNotEmptyError{}) - assert.True(t, errors.Is(err, fs.ErrInvalid)) + assert.ErrorIs(t, err, fs.ErrInvalid) // Delete should succeed for a non-empty directory if the DeleteRecursively flag is set. err = f.Delete(ctx, "/foo", filer.DeleteRecursively) @@ -224,8 +222,8 @@ func commonFilerReadWriteTests(t *testing.T, ctx context.Context, f filer.Filer) // Delete of the filer root should ALWAYS fail, otherwise subsequent writes would fail. // It is not in the filer's purview to delete its root directory. err = f.Delete(ctx, "/") - assert.True(t, errors.As(err, &filer.CannotDeleteRootError{})) - assert.True(t, errors.Is(err, fs.ErrInvalid)) + assert.ErrorAs(t, err, &filer.CannotDeleteRootError{}) + assert.ErrorIs(t, err, fs.ErrInvalid) } func TestFilerReadWrite(t *testing.T) { @@ -262,7 +260,7 @@ func commonFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { // We start with an empty directory. entries, err := f.ReadDir(ctx, ".") require.NoError(t, err) - assert.Len(t, entries, 0) + assert.Empty(t, entries) // Write a file. err = f.Write(ctx, "/hello.txt", strings.NewReader(`hello world`)) @@ -282,8 +280,8 @@ func commonFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { // Expect an error if the path doesn't exist. _, err = f.ReadDir(ctx, "/dir/a/b/c/d/e") - assert.True(t, errors.As(err, &filer.NoSuchDirectoryError{}), err) - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorAs(t, err, &filer.NoSuchDirectoryError{}, err) + assert.ErrorIs(t, err, fs.ErrNotExist) // Expect two entries in the root. entries, err = f.ReadDir(ctx, ".") @@ -295,7 +293,7 @@ func commonFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { assert.False(t, entries[1].IsDir()) info, err = entries[1].Info() require.NoError(t, err) - assert.Greater(t, info.ModTime().Unix(), int64(0)) + assert.Positive(t, info.ModTime().Unix()) // Expect two entries in the directory. entries, err = f.ReadDir(ctx, "/dir") @@ -307,7 +305,7 @@ func commonFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { assert.False(t, entries[1].IsDir()) info, err = entries[1].Info() require.NoError(t, err) - assert.Greater(t, info.ModTime().Unix(), int64(0)) + assert.Positive(t, info.ModTime().Unix()) // Expect a single entry in the nested path. entries, err = f.ReadDir(ctx, "/dir/a/b") @@ -325,7 +323,7 @@ func commonFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { require.NoError(t, err) entries, err = f.ReadDir(ctx, "empty-dir") assert.NoError(t, err) - assert.Len(t, entries, 0) + assert.Empty(t, entries) // Expect one entry for a directory with a file in it err = f.Write(ctx, "dir-with-one-file/my-file.txt", strings.NewReader("abc"), filer.CreateParentDirectories) @@ -333,7 +331,7 @@ func commonFilerReadDirTest(t *testing.T, ctx context.Context, f filer.Filer) { entries, err = f.ReadDir(ctx, "dir-with-one-file") assert.NoError(t, err) assert.Len(t, entries, 1) - assert.Equal(t, entries[0].Name(), "my-file.txt") + assert.Equal(t, "my-file.txt", entries[0].Name()) assert.False(t, entries[0].IsDir()) } @@ -459,7 +457,7 @@ func TestFilerWorkspaceNotebook(t *testing.T) { // Assert uploading a second time fails due to overwrite mode missing err = f.Write(ctx, tc.name, strings.NewReader(tc.content2)) require.ErrorIs(t, err, fs.ErrExist) - assert.Regexp(t, regexp.MustCompile(`file already exists: .*/`+tc.nameWithoutExt+`$`), err.Error()) + assert.Regexp(t, `file already exists: .*/`+tc.nameWithoutExt+`$`, err.Error()) // Try uploading the notebook again with overwrite flag. This time it should succeed. err = f.Write(ctx, tc.name, strings.NewReader(tc.content2), filer.OverwriteIfExists) diff --git a/libs/auth/cache/file_test.go b/libs/auth/cache/file_test.go index 3e4aae36f..54964bed3 100644 --- a/libs/auth/cache/file_test.go +++ b/libs/auth/cache/file_test.go @@ -42,7 +42,7 @@ func TestStoreAndLookup(t *testing.T) { tok, err := l.Lookup("x") require.NoError(t, err) assert.Equal(t, "abc", tok.AccessToken) - assert.Equal(t, 2, len(l.Tokens)) + assert.Len(t, l.Tokens, 2) _, err = l.Lookup("z") assert.Equal(t, ErrNotConfigured, err) diff --git a/libs/databrickscfg/ops_test.go b/libs/databrickscfg/ops_test.go index 3ea92024c..dd8484fb7 100644 --- a/libs/databrickscfg/ops_test.go +++ b/libs/databrickscfg/ops_test.go @@ -216,7 +216,7 @@ func TestSaveToProfile_ClearingPreviousProfile(t *testing.T) { dlft, err := file.GetSection("DEFAULT") assert.NoError(t, err) - assert.Len(t, dlft.KeysHash(), 0) + assert.Empty(t, dlft.KeysHash()) abc, err := file.GetSection("abc") assert.NoError(t, err) diff --git a/libs/databrickscfg/profile/file_test.go b/libs/databrickscfg/profile/file_test.go index 8e5cfefc0..6bcaec4b7 100644 --- a/libs/databrickscfg/profile/file_test.go +++ b/libs/databrickscfg/profile/file_test.go @@ -11,10 +11,10 @@ import ( ) func TestProfileCloud(t *testing.T) { - assert.Equal(t, Profile{Host: "https://dbc-XXXXXXXX-YYYY.cloud.databricks.com"}.Cloud(), "AWS") - assert.Equal(t, Profile{Host: "https://adb-xxx.y.azuredatabricks.net/"}.Cloud(), "Azure") - assert.Equal(t, Profile{Host: "https://workspace.gcp.databricks.com/"}.Cloud(), "GCP") - assert.Equal(t, Profile{Host: "https://some.invalid.host.com/"}.Cloud(), "AWS") + assert.Equal(t, "AWS", Profile{Host: "https://dbc-XXXXXXXX-YYYY.cloud.databricks.com"}.Cloud()) + assert.Equal(t, "Azure", Profile{Host: "https://adb-xxx.y.azuredatabricks.net/"}.Cloud()) + assert.Equal(t, "GCP", Profile{Host: "https://workspace.gcp.databricks.com/"}.Cloud()) + assert.Equal(t, "AWS", Profile{Host: "https://some.invalid.host.com/"}.Cloud()) } func TestProfilesSearchCaseInsensitive(t *testing.T) { diff --git a/libs/errs/aggregate_test.go b/libs/errs/aggregate_test.go index a307e956f..1af57e099 100644 --- a/libs/errs/aggregate_test.go +++ b/libs/errs/aggregate_test.go @@ -1,7 +1,6 @@ package errs import ( - "errors" "fmt" "testing" @@ -14,13 +13,13 @@ func TestFromManyErrors(t *testing.T) { e3 := fmt.Errorf("Error 3") err := FromMany(e1, e2, e3) - assert.True(t, errors.Is(err, e1)) - assert.True(t, errors.Is(err, e2)) - assert.True(t, errors.Is(err, e3)) + assert.ErrorIs(t, err, e1) + assert.ErrorIs(t, err, e2) + assert.ErrorIs(t, err, e3) - assert.Equal(t, err.Error(), `Error 1 + assert.Equal(t, `Error 1 Error 2 -Error 3`) +Error 3`, err.Error()) } func TestFromManyErrorsWihtNil(t *testing.T) { @@ -29,9 +28,9 @@ func TestFromManyErrorsWihtNil(t *testing.T) { e3 := fmt.Errorf("Error 3") err := FromMany(e1, e2, e3) - assert.True(t, errors.Is(err, e1)) - assert.True(t, errors.Is(err, e3)) + assert.ErrorIs(t, err, e1) + assert.ErrorIs(t, err, e3) - assert.Equal(t, err.Error(), `Error 1 -Error 3`) + assert.Equal(t, `Error 1 +Error 3`, err.Error()) } diff --git a/libs/filer/completer/completer_test.go b/libs/filer/completer/completer_test.go index d284447b9..865d34c2f 100644 --- a/libs/filer/completer/completer_test.go +++ b/libs/filer/completer/completer_test.go @@ -37,7 +37,7 @@ func TestFilerCompleterSetsPrefix(t *testing.T) { assert.Equal(t, []string{"dbfs:/dir/dirA/", "dbfs:/dir/dirB/"}, completions) assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive) - assert.Nil(t, err) + assert.NoError(t, err) } func TestFilerCompleterReturnsNestedDirs(t *testing.T) { @@ -46,7 +46,7 @@ func TestFilerCompleterReturnsNestedDirs(t *testing.T) { assert.Equal(t, []string{"dir/dirA/", "dir/dirB/"}, completions) assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive) - assert.Nil(t, err) + assert.NoError(t, err) } func TestFilerCompleterReturnsAdjacentDirs(t *testing.T) { @@ -55,7 +55,7 @@ func TestFilerCompleterReturnsAdjacentDirs(t *testing.T) { assert.Equal(t, []string{"dir/dirA/", "dir/dirB/"}, completions) assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive) - assert.Nil(t, err) + assert.NoError(t, err) } func TestFilerCompleterReturnsNestedDirsAndFiles(t *testing.T) { @@ -64,7 +64,7 @@ func TestFilerCompleterReturnsNestedDirsAndFiles(t *testing.T) { assert.Equal(t, []string{"dir/dirA/", "dir/dirB/", "dir/fileA"}, completions) assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive) - assert.Nil(t, err) + assert.NoError(t, err) } func TestFilerCompleterAddsDbfsPath(t *testing.T) { @@ -78,7 +78,7 @@ func TestFilerCompleterAddsDbfsPath(t *testing.T) { assert.Equal(t, []string{"dir/dirA/", "dir/dirB/", "dbfs:/"}, completions) assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive) - assert.Nil(t, err) + assert.NoError(t, err) } func TestFilerCompleterWindowsSeparator(t *testing.T) { @@ -92,7 +92,7 @@ func TestFilerCompleterWindowsSeparator(t *testing.T) { assert.Equal(t, []string{"dir\\dirA\\", "dir\\dirB\\", "dbfs:/"}, completions) assert.Equal(t, cobra.ShellCompDirectiveNoSpace, directive) - assert.Nil(t, err) + assert.NoError(t, err) } func TestFilerCompleterNoCompletions(t *testing.T) { diff --git a/libs/filer/fs_test.go b/libs/filer/fs_test.go index 849cf6f7c..08d7a9428 100644 --- a/libs/filer/fs_test.go +++ b/libs/filer/fs_test.go @@ -63,7 +63,7 @@ func TestFsOpenFile(t *testing.T) { assert.Equal(t, "fileA", info.Name()) assert.Equal(t, int64(3), info.Size()) assert.Equal(t, fs.FileMode(0), info.Mode()) - assert.Equal(t, false, info.IsDir()) + assert.False(t, info.IsDir()) // Read until closed. b := make([]byte, 3) @@ -91,7 +91,7 @@ func TestFsOpenDir(t *testing.T) { info, err := fakeFile.Stat() require.NoError(t, err) assert.Equal(t, "root", info.Name()) - assert.Equal(t, true, info.IsDir()) + assert.True(t, info.IsDir()) de, ok := fakeFile.(fs.ReadDirFile) require.True(t, ok) diff --git a/libs/fileset/glob_test.go b/libs/fileset/glob_test.go index 9eb786db9..0224b2547 100644 --- a/libs/fileset/glob_test.go +++ b/libs/fileset/glob_test.go @@ -52,7 +52,7 @@ func TestGlobFileset(t *testing.T) { files, err = g.Files() require.NoError(t, err) - require.Equal(t, len(files), 0) + require.Empty(t, files) } func TestGlobFilesetWithRelativeRoot(t *testing.T) { diff --git a/libs/flags/json_flag_test.go b/libs/flags/json_flag_test.go index b31324011..956a3541c 100644 --- a/libs/flags/json_flag_test.go +++ b/libs/flags/json_flag_test.go @@ -123,12 +123,12 @@ func TestJsonUnmarshalForRequest(t *testing.T) { 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.Len(t, r.NewSettings.Tasks, 1) 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) + assert.True(t, r.NewSettings.Tasks[0].RetryOnTimeout) } const incorrectJsonData = `{ @@ -280,8 +280,8 @@ func TestJsonUnmarshalForRequestWithForceSendFields(t *testing.T) { 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.False(t, r.NewSettings.NotificationSettings.NoAlertForSkippedRuns) + assert.False(t, r.NewSettings.NotificationSettings.NoAlertForCanceledRuns) assert.NotContains(t, r.NewSettings.NotificationSettings.ForceSendFields, "NoAlertForSkippedRuns") assert.Contains(t, r.NewSettings.NotificationSettings.ForceSendFields, "NoAlertForCanceledRuns") } diff --git a/libs/folders/folders_test.go b/libs/folders/folders_test.go index 17afc4022..d2afc4f2d 100644 --- a/libs/folders/folders_test.go +++ b/libs/folders/folders_test.go @@ -33,6 +33,6 @@ func TestFindDirWithLeaf(t *testing.T) { { out, err := FindDirWithLeaf(root, "this-leaf-doesnt-exist-anywhere") assert.ErrorIs(t, err, os.ErrNotExist) - assert.Equal(t, out, "") + assert.Equal(t, "", out) } } diff --git a/libs/jsonschema/utils_test.go b/libs/jsonschema/utils_test.go index 89200dae3..954c723d3 100644 --- a/libs/jsonschema/utils_test.go +++ b/libs/jsonschema/utils_test.go @@ -96,7 +96,7 @@ func TestTemplateFromString(t *testing.T) { v, err = fromString("1.1", NumberType) assert.NoError(t, err) // Floating point conversions are not perfect - assert.True(t, (v.(float64)-1.1) < 0.000001) + assert.Less(t, (v.(float64) - 1.1), 0.000001) v, err = fromString("12345", IntegerType) assert.NoError(t, err) @@ -104,7 +104,7 @@ func TestTemplateFromString(t *testing.T) { v, err = fromString("123", NumberType) assert.NoError(t, err) - assert.Equal(t, float64(123), v) + assert.InDelta(t, float64(123), v.(float64), 0.0001) _, err = fromString("qrt", ArrayType) assert.EqualError(t, err, "cannot parse string as object of type array. Value of string: \"qrt\"") diff --git a/libs/notebook/detect_test.go b/libs/notebook/detect_test.go index 4ede7bf9b..49a67d2d3 100644 --- a/libs/notebook/detect_test.go +++ b/libs/notebook/detect_test.go @@ -1,7 +1,6 @@ package notebook import ( - "errors" "io/fs" "os" "path/filepath" @@ -53,7 +52,7 @@ func TestDetectCallsDetectJupyter(t *testing.T) { func TestDetectUnknownExtension(t *testing.T) { _, _, err := Detect("./testdata/doesntexist.foobar") - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) nb, _, err := Detect("./testdata/unknown_extension.foobar") require.NoError(t, err) @@ -62,7 +61,7 @@ func TestDetectUnknownExtension(t *testing.T) { func TestDetectNoExtension(t *testing.T) { _, _, err := Detect("./testdata/doesntexist") - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) nb, _, err := Detect("./testdata/no_extension") require.NoError(t, err) diff --git a/libs/process/background_test.go b/libs/process/background_test.go index 2e47e814b..7843375cf 100644 --- a/libs/process/background_test.go +++ b/libs/process/background_test.go @@ -95,7 +95,7 @@ func TestBackgroundNoStdin(t *testing.T) { func TestBackgroundFails(t *testing.T) { ctx := context.Background() _, err := Background(ctx, []string{"ls", "/dev/null/x"}) - assert.NotNil(t, err) + assert.Error(t, err) } func TestBackgroundFailsOnOption(t *testing.T) { diff --git a/libs/process/forwarded_test.go b/libs/process/forwarded_test.go index ddb79818f..71f0a6a63 100644 --- a/libs/process/forwarded_test.go +++ b/libs/process/forwarded_test.go @@ -27,7 +27,7 @@ func TestForwardedFails(t *testing.T) { err := Forwarded(ctx, []string{ "_non_existent_", }, strings.NewReader("abc\n"), &buf, &buf) - assert.NotNil(t, err) + assert.Error(t, err) } func TestForwardedFailsOnStdinPipe(t *testing.T) { @@ -39,5 +39,5 @@ func TestForwardedFailsOnStdinPipe(t *testing.T) { c.Stdin = strings.NewReader("x") return nil }) - assert.NotNil(t, err) + assert.Error(t, err) } diff --git a/libs/process/opts_test.go b/libs/process/opts_test.go index 3a819fbb9..8b5d51928 100644 --- a/libs/process/opts_test.go +++ b/libs/process/opts_test.go @@ -41,7 +41,7 @@ func TestWorksWithLibsEnv(t *testing.T) { vars := cmd.Environ() sort.Strings(vars) - assert.True(t, len(vars) >= 2) + assert.GreaterOrEqual(t, len(vars), 2) assert.Equal(t, "CCC=DDD", vars[0]) assert.Equal(t, "EEE=FFF", vars[1]) } diff --git a/libs/python/interpreters_unix_test.go b/libs/python/interpreters_unix_test.go index 8471644a1..57adc9279 100644 --- a/libs/python/interpreters_unix_test.go +++ b/libs/python/interpreters_unix_test.go @@ -18,7 +18,7 @@ func TestAtLeastOnePythonInstalled(t *testing.T) { assert.NoError(t, err) a := all.Latest() t.Logf("latest is: %s", a) - assert.True(t, len(all) > 0) + assert.NotEmpty(t, all) } func TestNoInterpretersFound(t *testing.T) { diff --git a/libs/sync/snapshot_test.go b/libs/sync/snapshot_test.go index eef526e58..4ba3874ae 100644 --- a/libs/sync/snapshot_test.go +++ b/libs/sync/snapshot_test.go @@ -51,7 +51,7 @@ func TestDiff(t *testing.T) { assert.NoError(t, err) change, err := state.diff(ctx, files) assert.NoError(t, err) - assert.Len(t, change.delete, 0) + assert.Empty(t, change.delete) assert.Len(t, change.put, 2) assert.Contains(t, change.put, "hello.txt") assert.Contains(t, change.put, "world.txt") @@ -67,7 +67,7 @@ func TestDiff(t *testing.T) { change, err = state.diff(ctx, files) assert.NoError(t, err) - assert.Len(t, change.delete, 0) + assert.Empty(t, change.delete) assert.Len(t, change.put, 1) assert.Contains(t, change.put, "world.txt") assertKeysOfMap(t, state.LastModifiedTimes, []string{"hello.txt", "world.txt"}) @@ -82,7 +82,7 @@ func TestDiff(t *testing.T) { change, err = state.diff(ctx, files) assert.NoError(t, err) assert.Len(t, change.delete, 1) - assert.Len(t, change.put, 0) + assert.Empty(t, change.put) assert.Contains(t, change.delete, "hello.txt") assertKeysOfMap(t, state.LastModifiedTimes, []string{"world.txt"}) assert.Equal(t, map[string]string{"world.txt": "world.txt"}, state.LocalToRemoteNames) @@ -145,8 +145,8 @@ func TestFolderDiff(t *testing.T) { assert.NoError(t, err) change, err := state.diff(ctx, files) assert.NoError(t, err) - assert.Len(t, change.delete, 0) - assert.Len(t, change.rmdir, 0) + assert.Empty(t, change.delete) + assert.Empty(t, change.rmdir) assert.Len(t, change.mkdir, 1) assert.Len(t, change.put, 1) assert.Contains(t, change.mkdir, "foo") @@ -159,8 +159,8 @@ func TestFolderDiff(t *testing.T) { assert.NoError(t, err) assert.Len(t, change.delete, 1) assert.Len(t, change.rmdir, 1) - assert.Len(t, change.mkdir, 0) - assert.Len(t, change.put, 0) + assert.Empty(t, change.mkdir) + assert.Empty(t, change.put) assert.Contains(t, change.delete, "foo/bar") assert.Contains(t, change.rmdir, "foo") } @@ -189,7 +189,7 @@ func TestPythonNotebookDiff(t *testing.T) { foo.Overwrite(t, "# Databricks notebook source\nprint(\"abc\")") change, err := state.diff(ctx, files) assert.NoError(t, err) - assert.Len(t, change.delete, 0) + assert.Empty(t, change.delete) assert.Len(t, change.put, 1) assert.Contains(t, change.put, "foo.py") assertKeysOfMap(t, state.LastModifiedTimes, []string{"foo.py"}) @@ -233,9 +233,9 @@ func TestPythonNotebookDiff(t *testing.T) { change, err = state.diff(ctx, files) assert.NoError(t, err) assert.Len(t, change.delete, 1) - assert.Len(t, change.put, 0) + assert.Empty(t, change.put) assert.Contains(t, change.delete, "foo") - assert.Len(t, state.LastModifiedTimes, 0) + assert.Empty(t, state.LastModifiedTimes) assert.Equal(t, map[string]string{}, state.LocalToRemoteNames) assert.Equal(t, map[string]string{}, state.RemoteToLocalNames) } @@ -264,7 +264,7 @@ func TestErrorWhenIdenticalRemoteName(t *testing.T) { assert.NoError(t, err) change, err := state.diff(ctx, files) assert.NoError(t, err) - assert.Len(t, change.delete, 0) + assert.Empty(t, change.delete) assert.Len(t, change.put, 2) assert.Contains(t, change.put, "foo.py") assert.Contains(t, change.put, "foo") @@ -300,7 +300,7 @@ func TestNoErrorRenameWithIdenticalRemoteName(t *testing.T) { assert.NoError(t, err) change, err := state.diff(ctx, files) assert.NoError(t, err) - assert.Len(t, change.delete, 0) + assert.Empty(t, change.delete) assert.Len(t, change.put, 1) assert.Contains(t, change.put, "foo.py") diff --git a/libs/sync/sync_test.go b/libs/sync/sync_test.go index 6168dc217..f30431770 100644 --- a/libs/sync/sync_test.go +++ b/libs/sync/sync_test.go @@ -59,7 +59,7 @@ func TestGetFileSet(t *testing.T) { fileList, err := s.GetFileList(ctx) require.NoError(t, err) - require.Equal(t, len(fileList), 10) + require.Len(t, fileList, 10) inc, err = fileset.NewGlobSet(root, []string{}) require.NoError(t, err) @@ -77,7 +77,7 @@ func TestGetFileSet(t *testing.T) { fileList, err = s.GetFileList(ctx) require.NoError(t, err) - require.Equal(t, len(fileList), 2) + require.Len(t, fileList, 2) inc, err = fileset.NewGlobSet(root, []string{"./.databricks/*.go"}) require.NoError(t, err) @@ -95,7 +95,7 @@ func TestGetFileSet(t *testing.T) { fileList, err = s.GetFileList(ctx) require.NoError(t, err) - require.Equal(t, len(fileList), 11) + require.Len(t, fileList, 11) } func TestRecursiveExclude(t *testing.T) { @@ -125,7 +125,7 @@ func TestRecursiveExclude(t *testing.T) { fileList, err := s.GetFileList(ctx) require.NoError(t, err) - require.Equal(t, len(fileList), 7) + require.Len(t, fileList, 7) } func TestNegateExclude(t *testing.T) { @@ -155,6 +155,6 @@ func TestNegateExclude(t *testing.T) { fileList, err := s.GetFileList(ctx) require.NoError(t, err) - require.Equal(t, len(fileList), 1) - require.Equal(t, fileList[0].Relative, "test/sub1/sub2/h.txt") + require.Len(t, fileList, 1) + require.Equal(t, "test/sub1/sub2/h.txt", fileList[0].Relative) } diff --git a/libs/template/config_test.go b/libs/template/config_test.go index a855019b6..515a0b9f5 100644 --- a/libs/template/config_test.go +++ b/libs/template/config_test.go @@ -24,7 +24,7 @@ func TestTemplateConfigAssignValuesFromFile(t *testing.T) { err = c.assignValuesFromFile(filepath.Join(testDir, "config.json")) if assert.NoError(t, err) { assert.Equal(t, int64(1), c.values["int_val"]) - assert.Equal(t, float64(2), c.values["float_val"]) + assert.InDelta(t, float64(2), c.values["float_val"].(float64), 0.0001) assert.Equal(t, true, c.values["bool_val"]) assert.Equal(t, "hello", c.values["string_val"]) } @@ -44,7 +44,7 @@ func TestTemplateConfigAssignValuesFromFileDoesNotOverwriteExistingConfigs(t *te err = c.assignValuesFromFile(filepath.Join(testDir, "config.json")) if assert.NoError(t, err) { assert.Equal(t, int64(1), c.values["int_val"]) - assert.Equal(t, float64(2), c.values["float_val"]) + assert.InDelta(t, float64(2), c.values["float_val"].(float64), 0.0001) assert.Equal(t, true, c.values["bool_val"]) assert.Equal(t, "this-is-not-overwritten", c.values["string_val"]) } @@ -89,7 +89,7 @@ func TestTemplateConfigAssignValuesFromDefaultValues(t *testing.T) { err = c.assignDefaultValues(r) if assert.NoError(t, err) { assert.Equal(t, int64(123), c.values["int_val"]) - assert.Equal(t, float64(123), c.values["float_val"]) + assert.InDelta(t, float64(123), c.values["float_val"].(float64), 0.0001) assert.Equal(t, true, c.values["bool_val"]) assert.Equal(t, "hello", c.values["string_val"]) } @@ -110,7 +110,7 @@ func TestTemplateConfigAssignValuesFromTemplatedDefaultValues(t *testing.T) { err = c.assignDefaultValues(r) if assert.NoError(t, err) { assert.Equal(t, int64(123), c.values["int_val"]) - assert.Equal(t, float64(123), c.values["float_val"]) + assert.InDelta(t, float64(123), c.values["float_val"].(float64), 0.0001) assert.Equal(t, true, c.values["bool_val"]) assert.Equal(t, "world", c.values["string_val"]) } diff --git a/libs/template/helpers_test.go b/libs/template/helpers_test.go index d98f40b24..f8bc1f3da 100644 --- a/libs/template/helpers_test.go +++ b/libs/template/helpers_test.go @@ -86,7 +86,7 @@ func TestTemplateRandIntFunction(t *testing.T) { assert.Len(t, r.files, 1) randInt, err := strconv.Atoi(strings.TrimSpace(string(r.files[0].(*inMemoryFile).content))) assert.Less(t, randInt, 10) - assert.Empty(t, err) + assert.NoError(t, err) } func TestTemplateUuidFunction(t *testing.T) { diff --git a/libs/template/renderer_test.go b/libs/template/renderer_test.go index eeb308732..2c14009ff 100644 --- a/libs/template/renderer_test.go +++ b/libs/template/renderer_test.go @@ -434,7 +434,7 @@ func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) { entries, err := os.ReadDir(tmpDir) require.NoError(t, err) // Assert none of the files are persisted to disk, because of {{skip "*"}} - assert.Len(t, entries, 0) + assert.Empty(t, entries) } func TestRendererSkipPatternsAreRelativeToFileDirectory(t *testing.T) { @@ -588,8 +588,8 @@ func TestRendererNonTemplatesAreCreatedAsCopyFiles(t *testing.T) { assert.NoError(t, err) assert.Len(t, r.files, 1) - assert.Equal(t, r.files[0].(*copyFile).srcPath, "not-a-template") - assert.Equal(t, r.files[0].RelPath(), "not-a-template") + assert.Equal(t, "not-a-template", r.files[0].(*copyFile).srcPath) + assert.Equal(t, "not-a-template", r.files[0].RelPath()) } func TestRendererFileTreeRendering(t *testing.T) { @@ -609,7 +609,7 @@ func TestRendererFileTreeRendering(t *testing.T) { // Assert in memory representation is created. assert.Len(t, r.files, 1) - assert.Equal(t, r.files[0].RelPath(), "my_directory/my_file") + assert.Equal(t, "my_directory/my_file", r.files[0].RelPath()) out, err := filer.NewLocalClient(tmpDir) require.NoError(t, err) diff --git a/libs/vfs/filer_test.go b/libs/vfs/filer_test.go index ee1397521..6987c288e 100644 --- a/libs/vfs/filer_test.go +++ b/libs/vfs/filer_test.go @@ -2,7 +2,6 @@ package vfs import ( "context" - "errors" "io/fs" "os" "path/filepath" @@ -42,7 +41,7 @@ func TestFilerPath(t *testing.T) { // Open non-existent file. _, err = p.Open("doesntexist_test.go") - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) // Stat self. s, err = p.Stat("filer_test.go") @@ -52,7 +51,7 @@ func TestFilerPath(t *testing.T) { // Stat non-existent file. _, err = p.Stat("doesntexist_test.go") - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) // ReadDir self. entries, err := p.ReadDir(".") @@ -61,7 +60,7 @@ func TestFilerPath(t *testing.T) { // ReadDir non-existent directory. _, err = p.ReadDir("doesntexist") - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) // ReadFile self. buf, err = p.ReadFile("filer_test.go") @@ -70,7 +69,7 @@ func TestFilerPath(t *testing.T) { // ReadFile non-existent file. _, err = p.ReadFile("doesntexist_test.go") - assert.True(t, errors.Is(err, fs.ErrNotExist)) + assert.ErrorIs(t, err, fs.ErrNotExist) // Parent self. pp := p.Parent() From ef86d2bcaeff4d0e0f1445bf0e834299a63eadd7 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jan 2025 12:06:01 +0100 Subject: [PATCH 14/53] Speed up best case for "make test" 12x (#2060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On main branch: ‘make test’ takes about 33s On this branch: ‘make test’ takes about 2.7s (all measurements are for hot cache) What’s done (from highest impact to lowest): - Remove -coverprofile= option - this option was disabling "go test"'s built-in cache and also it took extra time to calculate the coverage (extra 21s). - Exclude ./integration/ folder, there are no unit tests there, but having it included adds significant time. "go test"'s caching also does not work there for me, due to TestMain() presence (extra 7.2s). - Remove dependency on "make lint" - nice to have, but slow to re-check the whole repo and should already be done by IDE (extra 2.5s). - Remove dependency on "make vendor" — rarely needed; on CI it is already executed separately (extra 1.1s). The coverage option is still available under "make cover". Use "make showcover" to show it. I’ve also removed separate "make testonly". If you only want tests, run "make test". If you want lint+test run "make lint test" etc. I've also modified the test command, removed unnecessary -short, -v, --raw-command. --- .github/workflows/push.yml | 2 +- Makefile | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 25bd5f4d6..f7c4d5456 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -55,7 +55,7 @@ jobs: pip3 install wheel - name: Run tests - run: make testonly + run: make test golangci: name: lint diff --git a/Makefile b/Makefile index 7dca3b2cf..2e7e34299 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,24 @@ default: build -lint: vendor +PACKAGES=./libs/... ./internal/... ./cmd/... ./bundle/... . + +lint: @echo "✓ Linting source code with https://golangci-lint.run/ (with --fix)..." @./lint.sh ./... -lintcheck: vendor +lintcheck: @echo "✓ Linting source code with https://golangci-lint.run/ ..." @golangci-lint run ./... -test: lint testonly - -testonly: +test: @echo "✓ Running tests ..." - @gotestsum --format pkgname-and-test-fails --no-summary=skipped --raw-command go test -v -json -short -coverprofile=coverage.txt ./... + @gotestsum --format pkgname-and-test-fails --no-summary=skipped -- ${PACKAGES} -coverage: test +cover: + @echo "✓ Running tests with coverage..." + @gotestsum --format pkgname-and-test-fails --no-summary=skipped -- -coverprofile=coverage.txt ${PACKAGES} + +showcover: @echo "✓ Opening coverage for unit tests ..." @go tool cover -html=coverage.txt @@ -42,4 +46,4 @@ integration: integration-short: $(INTEGRATION) -short -.PHONY: lint lintcheck test testonly coverage build snapshot vendor schema integration integration-short +.PHONY: lint lintcheck test cover showcover build snapshot vendor schema integration integration-short From ea8445af9eea3a6416c5c3705f664a5f8b66d085 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jan 2025 12:18:38 +0100 Subject: [PATCH 15/53] Make "make" output the commands it runs (#2066) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is useful on CI and locally for debugging and being able to copy-paste command to tweak the options. Removed redundant and imprecise messages like "✓ Running tests ...". --- Makefile | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 2e7e34299..b6a62765f 100644 --- a/Makefile +++ b/Makefile @@ -3,40 +3,31 @@ default: build PACKAGES=./libs/... ./internal/... ./cmd/... ./bundle/... . lint: - @echo "✓ Linting source code with https://golangci-lint.run/ (with --fix)..." - @./lint.sh ./... + ./lint.sh ./... lintcheck: - @echo "✓ Linting source code with https://golangci-lint.run/ ..." - @golangci-lint run ./... + golangci-lint run ./... test: - @echo "✓ Running tests ..." - @gotestsum --format pkgname-and-test-fails --no-summary=skipped -- ${PACKAGES} + gotestsum --format pkgname-and-test-fails --no-summary=skipped -- ${PACKAGES} cover: - @echo "✓ Running tests with coverage..." - @gotestsum --format pkgname-and-test-fails --no-summary=skipped -- -coverprofile=coverage.txt ${PACKAGES} + gotestsum --format pkgname-and-test-fails --no-summary=skipped -- -coverprofile=coverage.txt ${PACKAGES} showcover: - @echo "✓ Opening coverage for unit tests ..." - @go tool cover -html=coverage.txt + go tool cover -html=coverage.txt build: vendor - @echo "✓ Building source code with go build ..." - @go build -mod vendor + go build -mod vendor snapshot: - @echo "✓ Building dev snapshot" - @go build -o .databricks/databricks + go build -o .databricks/databricks vendor: - @echo "✓ Filling vendor folder with library code ..." - @go mod vendor + go mod vendor schema: - @echo "✓ Generating json-schema ..." - @go run ./bundle/internal/schema ./bundle/internal/schema ./bundle/schema/jsonschema.json + go run ./bundle/internal/schema ./bundle/internal/schema ./bundle/schema/jsonschema.json INTEGRATION = gotestsum --format github-actions --rerun-fails --jsonfile output.json --packages "./integration/..." -- -parallel 4 -timeout=2h From 890c57eabeb6252a98bbdfe7fe5a061a6284d48f Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:52:33 +0530 Subject: [PATCH 16/53] Enable debugging integration tests in VS Code (#2053) ## Changes This PR adds back debugging functionality that was lost during migration to `internal.Main` as an entry point for integration tests. The PR that caused the regression: https://github.com/databricks/cli/pull/2009. Specifically the addition of internal.Main as the entrypoint for all integration tests. ## Tests Manually, by trying to debug a test. --- integration/internal/acc/debug.go | 4 ++-- integration/internal/main.go | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/integration/internal/acc/debug.go b/integration/internal/acc/debug.go index b4939881e..08e385b09 100644 --- a/integration/internal/acc/debug.go +++ b/integration/internal/acc/debug.go @@ -11,14 +11,14 @@ import ( ) // Detects if test is run from "debug test" feature in VS Code. -func isInDebug() bool { +func IsInDebug() bool { ex, _ := os.Executable() return strings.HasPrefix(path.Base(ex), "__debug_bin") } // Loads debug environment from ~/.databricks/debug-env.json. func loadDebugEnvIfRunFromIDE(t testutil.TestingT, key string) { - if !isInDebug() { + if !IsInDebug() { return } home, err := os.UserHomeDir() diff --git a/integration/internal/main.go b/integration/internal/main.go index 6d69dcf70..6aa2a4c93 100644 --- a/integration/internal/main.go +++ b/integration/internal/main.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "testing" + + "github.com/databricks/cli/integration/internal/acc" ) // Main is the entry point for integration tests. @@ -11,7 +13,7 @@ import ( // they are not inadvertently executed when calling `go test ./...`. func Main(m *testing.M) { value := os.Getenv("CLOUD_ENV") - if value == "" { + if value == "" && !acc.IsInDebug() { fmt.Println("CLOUD_ENV is not set, skipping integration tests") return } From cae21693bbac8b74204a1f36b4c54ec66553de39 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jan 2025 12:23:48 +0100 Subject: [PATCH 17/53] lint: Raise max issues output (#2067) By default it stops after 3 issues of a given type, which gives false impression and also unhelpful if you fixing it with aider. 1000 is almost like unlimited but not unlimited in case there is a bug in a linter. --- .golangci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.golangci.yaml b/.golangci.yaml index fd83e0882..3e9a88957 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -40,3 +40,5 @@ linters-settings: - require-error issues: exclude-dirs-use-default: false # recommended by docs https://golangci-lint.run/usage/false-positives/ + max-issues-per-linter: 1000 + max-same-issues: 1000 From 509f5aba6ad74765e97e87adf34694967da119f6 Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:09:11 +0530 Subject: [PATCH 18/53] Snooze mlops-stacks integration test (#2063) ## Changes https://github.com/databricks/mlops-stacks/pull/187 broke mlops-stacks deployments for non-UC projects. Snoozing the test until upstream is fixed. ## Tests The test is skipped on my local machine. CI run will verify it's also skipped on Github Actions runners. --- integration/bundle/init_test.go | 2 ++ internal/testutil/helpers.go | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/integration/bundle/init_test.go b/integration/bundle/init_test.go index f5c263ca3..bc81cd3a8 100644 --- a/integration/bundle/init_test.go +++ b/integration/bundle/init_test.go @@ -39,6 +39,8 @@ func TestBundleInitErrorOnUnknownFields(t *testing.T) { // make changes that can break the MLOps Stacks DAB. In which case we should // skip this test until the MLOps Stacks DAB is updated to work again. func TestBundleInitOnMlopsStacks(t *testing.T) { + testutil.SkipUntil(t, "2025-01-09") + ctx, wt := acc.WorkspaceTest(t) w := wt.W diff --git a/internal/testutil/helpers.go b/internal/testutil/helpers.go index 019a8e618..44c2c9375 100644 --- a/internal/testutil/helpers.go +++ b/internal/testutil/helpers.go @@ -5,6 +5,9 @@ import ( "math/rand" "os" "strings" + "time" + + "github.com/stretchr/testify/require" ) // GetEnvOrSkipTest proceeds with test only with that env variable. @@ -30,3 +33,12 @@ func RandomName(prefix ...string) string { } return string(b) } + +func SkipUntil(t TestingT, date string) { + deadline, err := time.Parse(time.DateOnly, date) + require.NoError(t, err) + + if time.Now().Before(deadline) { + t.Skipf("Skipping test until %s. Time right now: %s", deadline.Format(time.DateOnly), time.Now()) + } +} From 7beb0fb8b5bf6f5eb7f11df7b958f713356ca33d Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:23:15 +0530 Subject: [PATCH 19/53] Add validation mutator for volume `artifact_path` (#2050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes This PR: 1. Incrementally improves the error messages shown to the user when the volume they are referring to in `workspace.artifact_path` does not exist. 2. Performs this validation in both `bundle validate` and `bundle deploy` compared to before on just deployments. 3. It runs "fast" validations on `bundle deploy`, which earlier were only run on `bundle validate`. ## Tests Unit tests and manually. Also, existing integration tests provide coverage (`TestUploadArtifactToVolumeNotYetDeployed`, `TestUploadArtifactFileToVolumeThatDoesNotExist`) Examples: ``` .venv➜ bundle-playground git:(master) ✗ cli bundle validate Error: cannot access volume capital.whatever.my_volume: User does not have READ VOLUME on Volume 'capital.whatever.my_volume'. at workspace.artifact_path in databricks.yml:7:18 ``` and ``` .venv➜ bundle-playground git:(master) ✗ cli bundle validate Error: volume capital.whatever.foobar does not exist at workspace.artifact_path resources.volumes.foo in databricks.yml:7:18 databricks.yml:12:7 You are using a volume in your artifact_path that is managed by this bundle but which has not been deployed yet. Please first deploy the volume using 'bundle deploy' and then switch over to using it in the artifact_path. ``` --- bundle/config/validate/fast_validate.go | 51 ++++ bundle/config/validate/validate.go | 9 +- .../config/validate/validate_artifact_path.go | 129 +++++++++ .../validate/validate_artifact_path_test.go | 244 ++++++++++++++++ bundle/libraries/filer.go | 2 +- bundle/libraries/filer_test.go | 8 - bundle/libraries/filer_volume.go | 124 +-------- bundle/libraries/filer_volume_test.go | 262 +----------------- bundle/libraries/upload_test.go | 7 - cmd/bundle/deploy.go | 2 + integration/bundle/artifacts_test.go | 4 +- 11 files changed, 444 insertions(+), 398 deletions(-) create mode 100644 bundle/config/validate/fast_validate.go create mode 100644 bundle/config/validate/validate_artifact_path.go create mode 100644 bundle/config/validate/validate_artifact_path_test.go diff --git a/bundle/config/validate/fast_validate.go b/bundle/config/validate/fast_validate.go new file mode 100644 index 000000000..47d83036d --- /dev/null +++ b/bundle/config/validate/fast_validate.go @@ -0,0 +1,51 @@ +package validate + +import ( + "context" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" +) + +// FastValidate runs a subset of fast validation checks. This is a subset of the full +// suite of validation mutators that satisfy ANY ONE of the following criteria: +// +// 1. No file i/o or network requests are made in the mutator. +// 2. The validation is blocking for bundle deployments. +// +// The full suite of validation mutators is available in the [Validate] mutator. +type fastValidateReadonly struct{} + +func FastValidateReadonly() bundle.ReadOnlyMutator { + return &fastValidateReadonly{} +} + +func (f *fastValidateReadonly) Name() string { + return "fast_validate(readonly)" +} + +func (f *fastValidateReadonly) Apply(ctx context.Context, rb bundle.ReadOnlyBundle) diag.Diagnostics { + return bundle.ApplyReadOnly(ctx, rb, bundle.Parallel( + // Fast mutators with only in-memory checks + JobClusterKeyDefined(), + JobTaskClusterSpec(), + SingleNodeCluster(), + + // Blocking mutators. Deployments will fail if these checks fail. + ValidateArtifactPath(), + )) +} + +type fastValidate struct{} + +func FastValidate() bundle.Mutator { + return &fastValidate{} +} + +func (f *fastValidate) Name() string { + return "fast_validate" +} + +func (f *fastValidate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + return bundle.ApplyReadOnly(ctx, bundle.ReadOnly(b), FastValidateReadonly()) +} diff --git a/bundle/config/validate/validate.go b/bundle/config/validate/validate.go index 131566fc9..8fdd704ab 100644 --- a/bundle/config/validate/validate.go +++ b/bundle/config/validate/validate.go @@ -30,12 +30,13 @@ func (l location) Path() dyn.Path { // Apply implements bundle.Mutator. func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { return bundle.ApplyReadOnly(ctx, bundle.ReadOnly(b), bundle.Parallel( - JobClusterKeyDefined(), + FastValidateReadonly(), + + // Slow mutators that require network or file i/o. These are only + // run in the `bundle validate` command. FilesToSync(), - ValidateSyncPatterns(), - JobTaskClusterSpec(), ValidateFolderPermissions(), - SingleNodeCluster(), + ValidateSyncPatterns(), )) } diff --git a/bundle/config/validate/validate_artifact_path.go b/bundle/config/validate/validate_artifact_path.go new file mode 100644 index 000000000..5bab99ccf --- /dev/null +++ b/bundle/config/validate/validate_artifact_path.go @@ -0,0 +1,129 @@ +package validate + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/libraries" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/dyn/dynvar" + "github.com/databricks/databricks-sdk-go/apierr" +) + +type validateArtifactPath struct{} + +func ValidateArtifactPath() bundle.ReadOnlyMutator { + return &validateArtifactPath{} +} + +func (v *validateArtifactPath) Name() string { + return "validate:artifact_paths" +} + +func extractVolumeFromPath(artifactPath string) (string, string, string, error) { + if !libraries.IsVolumesPath(artifactPath) { + return "", "", "", fmt.Errorf("expected artifact_path to start with /Volumes/, got %s", artifactPath) + } + + parts := strings.Split(artifactPath, "/") + volumeFormatErr := fmt.Errorf("expected UC volume path to be in the format /Volumes////..., got %s", artifactPath) + + // Incorrect format. + if len(parts) < 5 { + return "", "", "", volumeFormatErr + } + + catalogName := parts[2] + schemaName := parts[3] + volumeName := parts[4] + + // Incorrect format. + if catalogName == "" || schemaName == "" || volumeName == "" { + return "", "", "", volumeFormatErr + } + + return catalogName, schemaName, volumeName, nil +} + +func findVolumeInBundle(r config.Root, catalogName, schemaName, volumeName string) (dyn.Path, []dyn.Location, bool) { + volumes := r.Resources.Volumes + for k, v := range volumes { + if v.CatalogName != catalogName || v.Name != volumeName { + continue + } + // UC schemas can be defined in the bundle itself, and thus might be interpolated + // at runtime via the ${resources.schemas.} syntax. Thus we match the volume + // definition if the schema name is the same as the one in the bundle, or if the + // schema name is interpolated. + // We only have to check for ${resources.schemas...} references because any + // other valid reference (like ${var.foo}) would have been interpolated by this point. + p, ok := dynvar.PureReferenceToPath(v.SchemaName) + isSchemaDefinedInBundle := ok && p.HasPrefix(dyn.Path{dyn.Key("resources"), dyn.Key("schemas")}) + if v.SchemaName != schemaName && !isSchemaDefinedInBundle { + continue + } + pathString := fmt.Sprintf("resources.volumes.%s", k) + return dyn.MustPathFromString(pathString), r.GetLocations(pathString), true + } + return nil, nil, false +} + +func (v *validateArtifactPath) Apply(ctx context.Context, rb bundle.ReadOnlyBundle) diag.Diagnostics { + // We only validate UC Volumes paths right now. + if !libraries.IsVolumesPath(rb.Config().Workspace.ArtifactPath) { + return nil + } + + wrapErrorMsg := func(s string) diag.Diagnostics { + return diag.Diagnostics{ + { + Summary: s, + Severity: diag.Error, + Locations: rb.Config().GetLocations("workspace.artifact_path"), + Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, + }, + } + } + + catalogName, schemaName, volumeName, err := extractVolumeFromPath(rb.Config().Workspace.ArtifactPath) + if err != nil { + return wrapErrorMsg(err.Error()) + } + volumeFullName := fmt.Sprintf("%s.%s.%s", catalogName, schemaName, volumeName) + w := rb.WorkspaceClient() + _, err = w.Volumes.ReadByName(ctx, volumeFullName) + + if errors.Is(err, apierr.ErrPermissionDenied) { + return wrapErrorMsg(fmt.Sprintf("cannot access volume %s: %s", volumeFullName, err)) + } + if errors.Is(err, apierr.ErrNotFound) { + path, locations, ok := findVolumeInBundle(rb.Config(), catalogName, schemaName, volumeName) + if !ok { + return wrapErrorMsg(fmt.Sprintf("volume %s does not exist", volumeFullName)) + } + + // If the volume is defined in the bundle, provide a more helpful error diagnostic, + // with more details and location information. + return diag.Diagnostics{{ + Summary: fmt.Sprintf("volume %s does not exist", volumeFullName), + Severity: diag.Error, + Detail: `You are using a volume in your artifact_path that is managed by +this bundle but which has not been deployed yet. Please first deploy +the volume using 'bundle deploy' and then switch over to using it in +the artifact_path.`, + Locations: slices.Concat(rb.Config().GetLocations("workspace.artifact_path"), locations), + Paths: append([]dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, path), + }} + + } + if err != nil { + return wrapErrorMsg(fmt.Sprintf("cannot read volume %s: %s", volumeFullName, err)) + } + return nil +} diff --git a/bundle/config/validate/validate_artifact_path_test.go b/bundle/config/validate/validate_artifact_path_test.go new file mode 100644 index 000000000..8fb5c9618 --- /dev/null +++ b/bundle/config/validate/validate_artifact_path_test.go @@ -0,0 +1,244 @@ +package validate + +import ( + "context" + "fmt" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/internal/bundletest" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/experimental/mocks" + "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestValidateArtifactPathWithVolumeInBundle(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Workspace: config.Workspace{ + ArtifactPath: "/Volumes/catalogN/schemaN/volumeN/abc", + }, + Resources: config.Resources{ + Volumes: map[string]*resources.Volume{ + "foo": { + CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{ + CatalogName: "catalogN", + Name: "volumeN", + SchemaName: "schemaN", + }, + }, + }, + }, + }, + } + + bundletest.SetLocation(b, "workspace.artifact_path", []dyn.Location{{File: "file", Line: 1, Column: 1}}) + bundletest.SetLocation(b, "resources.volumes.foo", []dyn.Location{{File: "file", Line: 2, Column: 2}}) + + ctx := context.Background() + m := mocks.NewMockWorkspaceClient(t) + api := m.GetMockVolumesAPI() + api.EXPECT().ReadByName(mock.Anything, "catalogN.schemaN.volumeN").Return(nil, &apierr.APIError{ + StatusCode: 404, + }) + b.SetWorkpaceClient(m.WorkspaceClient) + + diags := bundle.ApplyReadOnly(ctx, bundle.ReadOnly(b), ValidateArtifactPath()) + assert.Equal(t, diag.Diagnostics{{ + Severity: diag.Error, + Summary: "volume catalogN.schemaN.volumeN does not exist", + Locations: []dyn.Location{ + {File: "file", Line: 1, Column: 1}, + {File: "file", Line: 2, Column: 2}, + }, + Paths: []dyn.Path{ + dyn.MustPathFromString("workspace.artifact_path"), + dyn.MustPathFromString("resources.volumes.foo"), + }, + Detail: `You are using a volume in your artifact_path that is managed by +this bundle but which has not been deployed yet. Please first deploy +the volume using 'bundle deploy' and then switch over to using it in +the artifact_path.`, + }}, diags) +} + +func TestValidateArtifactPath(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Workspace: config.Workspace{ + ArtifactPath: "/Volumes/catalogN/schemaN/volumeN/abc", + }, + }, + } + + bundletest.SetLocation(b, "workspace.artifact_path", []dyn.Location{{File: "file", Line: 1, Column: 1}}) + assertDiags := func(t *testing.T, diags diag.Diagnostics, expected string) { + assert.Len(t, diags, 1) + assert.Equal(t, diag.Diagnostics{{ + Severity: diag.Error, + Summary: expected, + Locations: []dyn.Location{{File: "file", Line: 1, Column: 1}}, + Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, + }}, diags) + } + + rb := bundle.ReadOnly(b) + ctx := context.Background() + + tcases := []struct { + err error + expectedSummary string + }{ + { + err: &apierr.APIError{ + StatusCode: 403, + Message: "User does not have USE SCHEMA on Schema 'catalogN.schemaN'", + }, + expectedSummary: "cannot access volume catalogN.schemaN.volumeN: User does not have USE SCHEMA on Schema 'catalogN.schemaN'", + }, + { + err: &apierr.APIError{ + StatusCode: 404, + }, + expectedSummary: "volume catalogN.schemaN.volumeN does not exist", + }, + { + err: &apierr.APIError{ + StatusCode: 500, + Message: "Internal Server Error", + }, + expectedSummary: "cannot read volume catalogN.schemaN.volumeN: Internal Server Error", + }, + } + + for _, tc := range tcases { + m := mocks.NewMockWorkspaceClient(t) + api := m.GetMockVolumesAPI() + api.EXPECT().ReadByName(mock.Anything, "catalogN.schemaN.volumeN").Return(nil, tc.err) + b.SetWorkpaceClient(m.WorkspaceClient) + + diags := bundle.ApplyReadOnly(ctx, rb, ValidateArtifactPath()) + assertDiags(t, diags, tc.expectedSummary) + } +} + +func invalidVolumePaths() []string { + return []string{ + "/Volumes/", + "/Volumes/main", + "/Volumes/main/", + "/Volumes/main//", + "/Volumes/main//my_schema", + "/Volumes/main/my_schema", + "/Volumes/main/my_schema/", + "/Volumes/main/my_schema//", + "/Volumes//my_schema/my_volume", + } +} + +func TestExtractVolumeFromPath(t *testing.T) { + catalogName, schemaName, volumeName, err := extractVolumeFromPath("/Volumes/main/my_schema/my_volume") + require.NoError(t, err) + assert.Equal(t, "main", catalogName) + assert.Equal(t, "my_schema", schemaName) + assert.Equal(t, "my_volume", volumeName) + + for _, p := range invalidVolumePaths() { + _, _, _, err := extractVolumeFromPath(p) + assert.EqualError(t, err, fmt.Sprintf("expected UC volume path to be in the format /Volumes////..., got %s", p)) + } +} + +func TestValidateArtifactPathWithInvalidPaths(t *testing.T) { + for _, p := range invalidVolumePaths() { + b := &bundle.Bundle{ + Config: config.Root{ + Workspace: config.Workspace{ + ArtifactPath: p, + }, + }, + } + + bundletest.SetLocation(b, "workspace.artifact_path", []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}) + + diags := bundle.ApplyReadOnly(context.Background(), bundle.ReadOnly(b), ValidateArtifactPath()) + require.Equal(t, diag.Diagnostics{{ + Severity: diag.Error, + Summary: fmt.Sprintf("expected UC volume path to be in the format /Volumes////..., got %s", p), + Locations: []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}, + Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, + }}, diags) + } +} + +func TestFindVolumeInBundle(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Volumes: map[string]*resources.Volume{ + "foo": { + CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{ + CatalogName: "main", + Name: "my_volume", + SchemaName: "my_schema", + }, + }, + }, + }, + }, + } + + bundletest.SetLocation(b, "resources.volumes.foo", []dyn.Location{ + { + File: "volume.yml", + Line: 1, + Column: 2, + }, + }) + + // volume is in DAB. + path, locations, ok := findVolumeInBundle(b.Config, "main", "my_schema", "my_volume") + assert.True(t, ok) + assert.Equal(t, []dyn.Location{{ + File: "volume.yml", + Line: 1, + Column: 2, + }}, locations) + assert.Equal(t, dyn.MustPathFromString("resources.volumes.foo"), path) + + // wrong volume name + _, _, ok = findVolumeInBundle(b.Config, "main", "my_schema", "doesnotexist") + assert.False(t, ok) + + // wrong schema name + _, _, ok = findVolumeInBundle(b.Config, "main", "doesnotexist", "my_volume") + assert.False(t, ok) + + // wrong catalog name + _, _, ok = findVolumeInBundle(b.Config, "doesnotexist", "my_schema", "my_volume") + assert.False(t, ok) + + // schema name is interpolated but does not have the right prefix. In this case + // we should not match the volume. + b.Config.Resources.Volumes["foo"].SchemaName = "${foo.bar.baz}" + _, _, ok = findVolumeInBundle(b.Config, "main", "my_schema", "my_volume") + assert.False(t, ok) + + // schema name is interpolated. + b.Config.Resources.Volumes["foo"].SchemaName = "${resources.schemas.my_schema.name}" + path, locations, ok = findVolumeInBundle(b.Config, "main", "valuedoesnotmatter", "my_volume") + assert.True(t, ok) + assert.Equal(t, []dyn.Location{{ + File: "volume.yml", + Line: 1, + Column: 2, + }}, locations) + assert.Equal(t, dyn.MustPathFromString("resources.volumes.foo"), path) +} diff --git a/bundle/libraries/filer.go b/bundle/libraries/filer.go index 4448ed325..e09c75e0e 100644 --- a/bundle/libraries/filer.go +++ b/bundle/libraries/filer.go @@ -24,7 +24,7 @@ func GetFilerForLibraries(ctx context.Context, b *bundle.Bundle) (filer.Filer, s switch { case IsVolumesPath(artifactPath): - return filerForVolume(ctx, b) + return filerForVolume(b) default: return filerForWorkspace(b) diff --git a/bundle/libraries/filer_test.go b/bundle/libraries/filer_test.go index 88ba152fc..c18da9726 100644 --- a/bundle/libraries/filer_test.go +++ b/bundle/libraries/filer_test.go @@ -7,10 +7,7 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/libs/filer" - sdkconfig "github.com/databricks/databricks-sdk-go/config" - "github.com/databricks/databricks-sdk-go/experimental/mocks" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -39,11 +36,6 @@ func TestGetFilerForLibrariesValidUcVolume(t *testing.T) { }, } - m := mocks.NewMockWorkspaceClient(t) - m.WorkspaceClient.Config = &sdkconfig.Config{} - m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/my_volume").Return(nil) - b.SetWorkpaceClient(m.WorkspaceClient) - client, uploadPath, diags := GetFilerForLibraries(context.Background(), b) require.NoError(t, diags.Error()) assert.Equal(t, "/Volumes/main/my_schema/my_volume/.internal", uploadPath) diff --git a/bundle/libraries/filer_volume.go b/bundle/libraries/filer_volume.go index aecf68db1..176f475c6 100644 --- a/bundle/libraries/filer_volume.go +++ b/bundle/libraries/filer_volume.go @@ -1,132 +1,16 @@ package libraries import ( - "context" - "errors" - "fmt" "path" - "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/dyn" - "github.com/databricks/cli/libs/dyn/dynvar" "github.com/databricks/cli/libs/filer" - "github.com/databricks/databricks-sdk-go/apierr" ) -func extractVolumeFromPath(artifactPath string) (string, string, string, error) { - if !IsVolumesPath(artifactPath) { - return "", "", "", fmt.Errorf("expected artifact_path to start with /Volumes/, got %s", artifactPath) - } - - parts := strings.Split(artifactPath, "/") - volumeFormatErr := fmt.Errorf("expected UC volume path to be in the format /Volumes////..., got %s", artifactPath) - - // Incorrect format. - if len(parts) < 5 { - return "", "", "", volumeFormatErr - } - - catalogName := parts[2] - schemaName := parts[3] - volumeName := parts[4] - - // Incorrect format. - if catalogName == "" || schemaName == "" || volumeName == "" { - return "", "", "", volumeFormatErr - } - - return catalogName, schemaName, volumeName, nil -} - -// This function returns a filer for ".internal" folder inside the directory configured -// at `workspace.artifact_path`. -// This function also checks if the UC volume exists in the workspace and then: -// 1. If the UC volume exists in the workspace: -// Returns a filer for the UC volume. -// 2. If the UC volume does not exist in the workspace but is (with high confidence) defined in -// the bundle configuration: -// Returns an error and a warning that instructs the user to deploy the -// UC volume before using it in the artifact path. -// 3. If the UC volume does not exist in the workspace and is not defined in the bundle configuration: -// Returns an error. -func filerForVolume(ctx context.Context, b *bundle.Bundle) (filer.Filer, string, diag.Diagnostics) { - artifactPath := b.Config.Workspace.ArtifactPath +func filerForVolume(b *bundle.Bundle) (filer.Filer, string, diag.Diagnostics) { w := b.WorkspaceClient() - - catalogName, schemaName, volumeName, err := extractVolumeFromPath(artifactPath) - if err != nil { - return nil, "", diag.Diagnostics{ - { - Severity: diag.Error, - Summary: err.Error(), - Locations: b.Config.GetLocations("workspace.artifact_path"), - Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, - }, - } - } - - // Check if the UC volume exists in the workspace. - volumePath := fmt.Sprintf("/Volumes/%s/%s/%s", catalogName, schemaName, volumeName) - err = w.Files.GetDirectoryMetadataByDirectoryPath(ctx, volumePath) - - // If the volume exists already, directly return the filer for the path to - // upload the artifacts to. - if err == nil { - uploadPath := path.Join(artifactPath, InternalDirName) - f, err := filer.NewFilesClient(w, uploadPath) - return f, uploadPath, diag.FromErr(err) - } - - baseErr := diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("unable to determine if volume at %s exists: %s", volumePath, err), - Locations: b.Config.GetLocations("workspace.artifact_path"), - Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, - } - - if errors.Is(err, apierr.ErrNotFound) { - // Since the API returned a 404, the volume does not exist. - // Modify the error message to provide more context. - baseErr.Summary = fmt.Sprintf("volume %s does not exist: %s", volumePath, err) - - // If the volume is defined in the bundle, provide a more helpful error diagnostic, - // with more details and location information. - path, locations, ok := findVolumeInBundle(b, catalogName, schemaName, volumeName) - if !ok { - return nil, "", diag.Diagnostics{baseErr} - } - baseErr.Detail = `You are using a volume in your artifact_path that is managed by -this bundle but which has not been deployed yet. Please first deploy -the volume using 'bundle deploy' and then switch over to using it in -the artifact_path.` - baseErr.Paths = append(baseErr.Paths, path) - baseErr.Locations = append(baseErr.Locations, locations...) - } - - return nil, "", diag.Diagnostics{baseErr} -} - -func findVolumeInBundle(b *bundle.Bundle, catalogName, schemaName, volumeName string) (dyn.Path, []dyn.Location, bool) { - volumes := b.Config.Resources.Volumes - for k, v := range volumes { - if v.CatalogName != catalogName || v.Name != volumeName { - continue - } - // UC schemas can be defined in the bundle itself, and thus might be interpolated - // at runtime via the ${resources.schemas.} syntax. Thus we match the volume - // definition if the schema name is the same as the one in the bundle, or if the - // schema name is interpolated. - // We only have to check for ${resources.schemas...} references because any - // other valid reference (like ${var.foo}) would have been interpolated by this point. - p, ok := dynvar.PureReferenceToPath(v.SchemaName) - isSchemaDefinedInBundle := ok && p.HasPrefix(dyn.Path{dyn.Key("resources"), dyn.Key("schemas")}) - if v.SchemaName != schemaName && !isSchemaDefinedInBundle { - continue - } - pathString := fmt.Sprintf("resources.volumes.%s", k) - return dyn.MustPathFromString(pathString), b.Config.GetLocations(pathString), true - } - return nil, nil, false + uploadPath := path.Join(b.Config.Workspace.ArtifactPath, InternalDirName) + f, err := filer.NewFilesClient(w, uploadPath) + return f, uploadPath, diag.FromErr(err) } diff --git a/bundle/libraries/filer_volume_test.go b/bundle/libraries/filer_volume_test.go index 2af54b0cb..39bdc4135 100644 --- a/bundle/libraries/filer_volume_test.go +++ b/bundle/libraries/filer_volume_test.go @@ -1,277 +1,27 @@ package libraries import ( - "context" - "fmt" "path" "testing" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/bundle/internal/bundletest" - "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/dyn" "github.com/databricks/cli/libs/filer" - "github.com/databricks/databricks-sdk-go/apierr" - sdkconfig "github.com/databricks/databricks-sdk-go/config" - "github.com/databricks/databricks-sdk-go/experimental/mocks" - "github.com/databricks/databricks-sdk-go/service/catalog" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) -func TestFindVolumeInBundle(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Volumes: map[string]*resources.Volume{ - "foo": { - CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{ - CatalogName: "main", - Name: "my_volume", - SchemaName: "my_schema", - }, - }, - }, - }, - }, - } - - bundletest.SetLocation(b, "resources.volumes.foo", []dyn.Location{ - { - File: "volume.yml", - Line: 1, - Column: 2, - }, - }) - - // volume is in DAB. - path, locations, ok := findVolumeInBundle(b, "main", "my_schema", "my_volume") - assert.True(t, ok) - assert.Equal(t, []dyn.Location{{ - File: "volume.yml", - Line: 1, - Column: 2, - }}, locations) - assert.Equal(t, dyn.MustPathFromString("resources.volumes.foo"), path) - - // wrong volume name - _, _, ok = findVolumeInBundle(b, "main", "my_schema", "doesnotexist") - assert.False(t, ok) - - // wrong schema name - _, _, ok = findVolumeInBundle(b, "main", "doesnotexist", "my_volume") - assert.False(t, ok) - - // wrong catalog name - _, _, ok = findVolumeInBundle(b, "doesnotexist", "my_schema", "my_volume") - assert.False(t, ok) - - // schema name is interpolated but does not have the right prefix. In this case - // we should not match the volume. - b.Config.Resources.Volumes["foo"].SchemaName = "${foo.bar.baz}" - _, _, ok = findVolumeInBundle(b, "main", "my_schema", "my_volume") - assert.False(t, ok) - - // schema name is interpolated. - b.Config.Resources.Volumes["foo"].SchemaName = "${resources.schemas.my_schema.name}" - path, locations, ok = findVolumeInBundle(b, "main", "valuedoesnotmatter", "my_volume") - assert.True(t, ok) - assert.Equal(t, []dyn.Location{{ - File: "volume.yml", - Line: 1, - Column: 2, - }}, locations) - assert.Equal(t, dyn.MustPathFromString("resources.volumes.foo"), path) -} - -func TestFilerForVolumeForErrorFromAPI(t *testing.T) { +func TestFilerForVolume(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Workspace: config.Workspace{ - ArtifactPath: "/Volumes/main/my_schema/my_volume", + ArtifactPath: "/Volumes/main/my_schema/my_volume/abc", }, }, } - bundletest.SetLocation(b, "workspace.artifact_path", []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}) - - m := mocks.NewMockWorkspaceClient(t) - m.WorkspaceClient.Config = &sdkconfig.Config{} - m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/my_volume").Return(fmt.Errorf("error from API")) - b.SetWorkpaceClient(m.WorkspaceClient) - - _, _, diags := filerForVolume(context.Background(), b) - assert.Equal(t, diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "unable to determine if volume at /Volumes/main/my_schema/my_volume exists: error from API", - Locations: []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}, - Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, - }, - }, diags) -} - -func TestFilerForVolumeWithVolumeNotFound(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Workspace: config.Workspace{ - ArtifactPath: "/Volumes/main/my_schema/doesnotexist", - }, - }, - } - - bundletest.SetLocation(b, "workspace.artifact_path", []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}) - - m := mocks.NewMockWorkspaceClient(t) - m.WorkspaceClient.Config = &sdkconfig.Config{} - m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/doesnotexist").Return(apierr.NotFound("some error message")) - b.SetWorkpaceClient(m.WorkspaceClient) - - _, _, diags := filerForVolume(context.Background(), b) - assert.Equal(t, diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "volume /Volumes/main/my_schema/doesnotexist does not exist: some error message", - Locations: []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}, - Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, - }, - }, diags) -} - -func TestFilerForVolumeNotFoundAndInBundle(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Workspace: config.Workspace{ - ArtifactPath: "/Volumes/main/my_schema/my_volume", - }, - Resources: config.Resources{ - Volumes: map[string]*resources.Volume{ - "foo": { - CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{ - CatalogName: "main", - Name: "my_volume", - VolumeType: "MANAGED", - SchemaName: "my_schema", - }, - }, - }, - }, - }, - } - - bundletest.SetLocation(b, "workspace.artifact_path", []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}) - bundletest.SetLocation(b, "resources.volumes.foo", []dyn.Location{{File: "volume.yml", Line: 1, Column: 2}}) - - m := mocks.NewMockWorkspaceClient(t) - m.WorkspaceClient.Config = &sdkconfig.Config{} - m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/my_volume").Return(apierr.NotFound("error from API")) - b.SetWorkpaceClient(m.WorkspaceClient) - - _, _, diags := GetFilerForLibraries(context.Background(), b) - assert.Equal(t, diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "volume /Volumes/main/my_schema/my_volume does not exist: error from API", - Locations: []dyn.Location{{File: "config.yml", Line: 1, Column: 2}, {File: "volume.yml", Line: 1, Column: 2}}, - Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path"), dyn.MustPathFromString("resources.volumes.foo")}, - Detail: `You are using a volume in your artifact_path that is managed by -this bundle but which has not been deployed yet. Please first deploy -the volume using 'bundle deploy' and then switch over to using it in -the artifact_path.`, - }, - }, diags) -} - -func invalidVolumePaths() []string { - return []string{ - "/Volumes/", - "/Volumes/main", - "/Volumes/main/", - "/Volumes/main//", - "/Volumes/main//my_schema", - "/Volumes/main/my_schema", - "/Volumes/main/my_schema/", - "/Volumes/main/my_schema//", - "/Volumes//my_schema/my_volume", - } -} - -func TestFilerForVolumeWithInvalidVolumePaths(t *testing.T) { - for _, p := range invalidVolumePaths() { - b := &bundle.Bundle{ - Config: config.Root{ - Workspace: config.Workspace{ - ArtifactPath: p, - }, - }, - } - - bundletest.SetLocation(b, "workspace.artifact_path", []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}) - - _, _, diags := GetFilerForLibraries(context.Background(), b) - require.Equal(t, diag.Diagnostics{{ - Severity: diag.Error, - Summary: fmt.Sprintf("expected UC volume path to be in the format /Volumes////..., got %s", p), - Locations: []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}, - Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, - }}, diags) - } -} - -func TestFilerForVolumeWithInvalidPrefix(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Workspace: config.Workspace{ - ArtifactPath: "/Volume/main/my_schema/my_volume", - }, - }, - } - - _, _, diags := filerForVolume(context.Background(), b) - require.EqualError(t, diags.Error(), "expected artifact_path to start with /Volumes/, got /Volume/main/my_schema/my_volume") -} - -func TestFilerForVolumeWithValidVolumePaths(t *testing.T) { - validPaths := []string{ - "/Volumes/main/my_schema/my_volume", - "/Volumes/main/my_schema/my_volume/", - "/Volumes/main/my_schema/my_volume/a/b/c", - "/Volumes/main/my_schema/my_volume/a/a/a", - } - - for _, p := range validPaths { - b := &bundle.Bundle{ - Config: config.Root{ - Workspace: config.Workspace{ - ArtifactPath: p, - }, - }, - } - - m := mocks.NewMockWorkspaceClient(t) - m.WorkspaceClient.Config = &sdkconfig.Config{} - m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/my_volume").Return(nil) - b.SetWorkpaceClient(m.WorkspaceClient) - - client, uploadPath, diags := filerForVolume(context.Background(), b) - require.NoError(t, diags.Error()) - assert.Equal(t, path.Join(p, ".internal"), uploadPath) - assert.IsType(t, &filer.FilesClient{}, client) - } -} - -func TestExtractVolumeFromPath(t *testing.T) { - catalogName, schemaName, volumeName, err := extractVolumeFromPath("/Volumes/main/my_schema/my_volume") - require.NoError(t, err) - assert.Equal(t, "main", catalogName) - assert.Equal(t, "my_schema", schemaName) - assert.Equal(t, "my_volume", volumeName) - - for _, p := range invalidVolumePaths() { - _, _, _, err := extractVolumeFromPath(p) - assert.EqualError(t, err, fmt.Sprintf("expected UC volume path to be in the format /Volumes////..., got %s", p)) - } + client, uploadPath, diags := filerForVolume(b) + require.NoError(t, diags.Error()) + assert.Equal(t, path.Join("/Volumes/main/my_schema/my_volume/abc/.internal"), uploadPath) + assert.IsType(t, &filer.FilesClient{}, client) } diff --git a/bundle/libraries/upload_test.go b/bundle/libraries/upload_test.go index 493785bf5..44b194c56 100644 --- a/bundle/libraries/upload_test.go +++ b/bundle/libraries/upload_test.go @@ -11,8 +11,6 @@ import ( mockfiler "github.com/databricks/cli/internal/mocks/libs/filer" "github.com/databricks/cli/internal/testutil" "github.com/databricks/cli/libs/filer" - sdkconfig "github.com/databricks/databricks-sdk-go/config" - "github.com/databricks/databricks-sdk-go/experimental/mocks" "github.com/databricks/databricks-sdk-go/service/compute" "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/stretchr/testify/mock" @@ -183,11 +181,6 @@ func TestArtifactUploadForVolumes(t *testing.T) { filer.CreateParentDirectories, ).Return(nil) - m := mocks.NewMockWorkspaceClient(t) - m.WorkspaceClient.Config = &sdkconfig.Config{} - m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/foo/bar/artifacts").Return(nil) - b.SetWorkpaceClient(m.WorkspaceClient) - diags := bundle.Apply(context.Background(), b, bundle.Seq(ExpandGlobReferences(), UploadWithClient(mockFiler))) require.NoError(t, diags.Error()) diff --git a/cmd/bundle/deploy.go b/cmd/bundle/deploy.go index a25e02f6c..560b07e39 100644 --- a/cmd/bundle/deploy.go +++ b/cmd/bundle/deploy.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config/validate" "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/bundle/render" "github.com/databricks/cli/cmd/bundle/utils" @@ -71,6 +72,7 @@ func newDeployCommand() *cobra.Command { diags = diags.Extend( bundle.Apply(ctx, b, bundle.Seq( phases.Initialize(), + validate.FastValidate(), phases.Build(), phases.Deploy(outputHandler), )), diff --git a/integration/bundle/artifacts_test.go b/integration/bundle/artifacts_test.go index 9dafe2f83..94b96899e 100644 --- a/integration/bundle/artifacts_test.go +++ b/integration/bundle/artifacts_test.go @@ -257,7 +257,7 @@ func TestUploadArtifactFileToVolumeThatDoesNotExist(t *testing.T) { stdout, stderr, err := testcli.RequireErrorRun(t, ctx, "bundle", "deploy") assert.Error(t, err) - assert.Equal(t, fmt.Sprintf(`Error: volume /Volumes/main/%s/doesnotexist does not exist: Not Found + assert.Equal(t, fmt.Sprintf(`Error: volume main.%s.doesnotexist does not exist at workspace.artifact_path in databricks.yml:6:18 @@ -293,7 +293,7 @@ func TestUploadArtifactToVolumeNotYetDeployed(t *testing.T) { stdout, stderr, err := testcli.RequireErrorRun(t, ctx, "bundle", "deploy") assert.Error(t, err) - assert.Equal(t, fmt.Sprintf(`Error: volume /Volumes/main/%s/my_volume does not exist: Not Found + assert.Equal(t, fmt.Sprintf(`Error: volume main.%s.my_volume does not exist at workspace.artifact_path resources.volumes.foo in databricks.yml:6:18 From 60782b57bdbe09af7cfe1232b827b4668ca92bb9 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Thu, 2 Jan 2025 14:23:00 +0100 Subject: [PATCH 20/53] Added close stale issues workflow (#634) ## Changes Added workflows for closing stale issues. It adds a Github Action that warns and then auto closes stale issues. --- .github/workflows/close-stale-issues.yml | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/close-stale-issues.yml diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml new file mode 100644 index 000000000..ffe550132 --- /dev/null +++ b/.github/workflows/close-stale-issues.yml @@ -0,0 +1,36 @@ +name: "Close Stale Issues" + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" # Run at midnight every day + +jobs: + cleanup: + permissions: + issues: write + contents: read + pull-requests: write + runs-on: ubuntu-latest + name: Stale issue job + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: This issue has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled. + stale-pr-message: This PR has not received an update in a while. If you want to keep this PR open, please leave a comment below or push a new commit and auto-close will be canceled. + + # These labels are required + stale-issue-label: Stale + stale-pr-label: Stale + + exempt-issue-labels: No Autoclose + exempt-pr-labels: No Autoclose + + # Issue timing + days-before-stale: 30 + days-before-close: 7 + + repo-token: ${{ secrets.GITHUB_TOKEN }} + loglevel: DEBUG + # TODO: Remove dry-run after merge when confirmed it works correctly + dry-run: true From 39d1e8093fcaf359c617c4f5580123a43f7f7ec9 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 3 Jan 2025 10:25:07 +0100 Subject: [PATCH 21/53] Enable intrange linter and apply autofix (#2069) New construct in Go1.22+ for integer iteration: https://github.com/ckaznocha/intrange?tab=readme-ov-file#intrange --- .golangci.yaml | 1 + bundle/config/mutator/process_target_mode_test.go | 4 ++-- bundle/config/resources_test.go | 6 +++--- bundle/config/variable/lookup_test.go | 2 +- bundle/deploy/terraform/convert_test.go | 2 +- bundle/deploy/terraform/state_push_test.go | 2 +- bundle/internal/schema/parser.go | 2 +- bundle/run/pipeline.go | 4 ++-- integration/libs/locker/locker_test.go | 12 ++++++------ integration/python/python_tasks_test.go | 6 +++--- libs/cmdio/render.go | 2 +- libs/cmdio/render_test.go | 2 +- libs/dyn/convert/from_typed.go | 2 +- libs/dyn/convert/struct_info.go | 2 +- libs/dyn/mapping_test.go | 2 +- libs/dyn/merge/override.go | 2 +- libs/dyn/path.go | 4 ++-- libs/dyn/visit_map_test.go | 8 ++++---- libs/exec/exec_test.go | 2 +- libs/filer/fs_test.go | 2 +- libs/jsonschema/from_type.go | 4 ++-- 21 files changed, 37 insertions(+), 36 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 3e9a88957..6558c502d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -12,6 +12,7 @@ linters: - gofumpt - goimports - testifylint + - intrange linters-settings: govet: enable-all: true diff --git a/bundle/config/mutator/process_target_mode_test.go b/bundle/config/mutator/process_target_mode_test.go index e1aa9e59b..c299a7636 100644 --- a/bundle/config/mutator/process_target_mode_test.go +++ b/bundle/config/mutator/process_target_mode_test.go @@ -382,7 +382,7 @@ func TestAllResourcesMocked(t *testing.T) { b := mockBundle(config.Development) resources := reflect.ValueOf(b.Config.Resources) - for i := 0; i < resources.NumField(); i++ { + for i := range resources.NumField() { field := resources.Field(i) if field.Kind() == reflect.Map { assert.True( @@ -411,7 +411,7 @@ func TestAllNonUcResourcesAreRenamed(t *testing.T) { require.NoError(t, diags.Error()) resources := reflect.ValueOf(b.Config.Resources) - for i := 0; i < resources.NumField(); i++ { + for i := range resources.NumField() { field := resources.Field(i) if field.Kind() == reflect.Map { diff --git a/bundle/config/resources_test.go b/bundle/config/resources_test.go index 3da645585..cbbcf5e27 100644 --- a/bundle/config/resources_test.go +++ b/bundle/config/resources_test.go @@ -33,7 +33,7 @@ func TestCustomMarshallerIsImplemented(t *testing.T) { r := Resources{} rt := reflect.TypeOf(r) - for i := 0; i < rt.NumField(); i++ { + for i := range rt.NumField() { field := rt.Field(i) // Fields in Resources are expected be of the form map[string]*resourceStruct @@ -75,7 +75,7 @@ func TestResourcesAllResourcesCompleteness(t *testing.T) { types = append(types, group.Description.PluralName) } - for i := 0; i < rt.NumField(); i++ { + for i := range rt.NumField() { field := rt.Field(i) jsonTag := field.Tag.Get("json") @@ -92,7 +92,7 @@ func TestSupportedResources(t *testing.T) { actual := SupportedResources() typ := reflect.TypeOf(Resources{}) - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { field := typ.Field(i) jsonTags := strings.Split(field.Tag.Get("json"), ",") pluralName := jsonTags[0] diff --git a/bundle/config/variable/lookup_test.go b/bundle/config/variable/lookup_test.go index bd54d89fc..bcfcb4626 100644 --- a/bundle/config/variable/lookup_test.go +++ b/bundle/config/variable/lookup_test.go @@ -13,7 +13,7 @@ func TestLookup_Coverage(t *testing.T) { val := reflect.ValueOf(lookup) typ := val.Type() - for i := 0; i < val.NumField(); i++ { + for i := range val.NumField() { field := val.Field(i) if field.Kind() != reflect.String { t.Fatalf("Field %s is not a string", typ.Field(i).Name) diff --git a/bundle/deploy/terraform/convert_test.go b/bundle/deploy/terraform/convert_test.go index 84b3c5788..ccfdcece3 100644 --- a/bundle/deploy/terraform/convert_test.go +++ b/bundle/deploy/terraform/convert_test.go @@ -1261,7 +1261,7 @@ func TestTerraformToBundleModifiedResources(t *testing.T) { func AssertFullResourceCoverage(t *testing.T, config *config.Root) { resources := reflect.ValueOf(config.Resources) - for i := 0; i < resources.NumField(); i++ { + for i := range resources.NumField() { field := resources.Field(i) if field.Kind() == reflect.Map { assert.True( diff --git a/bundle/deploy/terraform/state_push_test.go b/bundle/deploy/terraform/state_push_test.go index 4cc52b7a7..54e7f621c 100644 --- a/bundle/deploy/terraform/state_push_test.go +++ b/bundle/deploy/terraform/state_push_test.go @@ -71,7 +71,7 @@ func TestStatePushLargeState(t *testing.T) { b := statePushTestBundle(t) largeState := map[string]any{} - for i := 0; i < 1000000; i++ { + for i := range 1000000 { largeState[fmt.Sprintf("field_%d", i)] = i } diff --git a/bundle/internal/schema/parser.go b/bundle/internal/schema/parser.go index e1d1a13dc..919908429 100644 --- a/bundle/internal/schema/parser.go +++ b/bundle/internal/schema/parser.go @@ -54,7 +54,7 @@ func (p *openapiParser) findRef(typ reflect.Type) (jsonschema.Schema, bool) { // Check for embedded Databricks Go SDK types. if typ.Kind() == reflect.Struct { - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { if !typ.Field(i).Anonymous { continue } diff --git a/bundle/run/pipeline.go b/bundle/run/pipeline.go index d84015d76..c447f044a 100644 --- a/bundle/run/pipeline.go +++ b/bundle/run/pipeline.go @@ -17,7 +17,7 @@ import ( func filterEventsByUpdateId(events []pipelines.PipelineEvent, updateId string) []pipelines.PipelineEvent { result := []pipelines.PipelineEvent{} - for i := 0; i < len(events); i++ { + for i := range events { if events[i].Origin.UpdateId == updateId { result = append(result, events[i]) } @@ -32,7 +32,7 @@ func (r *pipelineRunner) logEvent(ctx context.Context, event pipelines.PipelineE } if event.Error != nil && len(event.Error.Exceptions) > 0 { logString += "trace for most recent exception: \n" - for i := 0; i < len(event.Error.Exceptions); i++ { + for i := range len(event.Error.Exceptions) { logString += fmt.Sprintf("%s\n", event.Error.Exceptions[i].Message) } } diff --git a/integration/libs/locker/locker_test.go b/integration/libs/locker/locker_test.go index c51972b90..524996465 100644 --- a/integration/libs/locker/locker_test.go +++ b/integration/libs/locker/locker_test.go @@ -60,13 +60,13 @@ func TestLock(t *testing.T) { lockerErrs := make([]error, numConcurrentLocks) lockers := make([]*lockpkg.Locker, numConcurrentLocks) - for i := 0; i < numConcurrentLocks; i++ { + for i := range numConcurrentLocks { lockers[i], err = lockpkg.CreateLocker("humpty.dumpty@databricks.com", remoteProjectRoot, wsc) require.NoError(t, err) } var wg sync.WaitGroup - for i := 0; i < numConcurrentLocks; i++ { + for i := range numConcurrentLocks { wg.Add(1) currentIndex := i go func() { @@ -80,7 +80,7 @@ func TestLock(t *testing.T) { countActive := 0 indexOfActiveLocker := 0 indexOfAnInactiveLocker := -1 - for i := 0; i < numConcurrentLocks; i++ { + for i := range numConcurrentLocks { if lockers[i].Active { countActive += 1 assert.NoError(t, lockerErrs[i]) @@ -102,7 +102,7 @@ func TestLock(t *testing.T) { assert.True(t, remoteLocker.AcquisitionTime.Equal(lockers[indexOfActiveLocker].State.AcquisitionTime), "remote locker acquisition time does not match active locker") // test all other locks (inactive ones) do not match the remote lock and Unlock fails - for i := 0; i < numConcurrentLocks; i++ { + for i := range numConcurrentLocks { if i == indexOfActiveLocker { continue } @@ -112,7 +112,7 @@ func TestLock(t *testing.T) { } // test inactive locks fail to write a file - for i := 0; i < numConcurrentLocks; i++ { + for i := range numConcurrentLocks { if i == indexOfActiveLocker { continue } @@ -140,7 +140,7 @@ func TestLock(t *testing.T) { assert.Equal(t, "Shah Rukh", res["name"]) // inactive locker file reads fail - for i := 0; i < numConcurrentLocks; i++ { + for i := range numConcurrentLocks { if i == indexOfActiveLocker { continue } diff --git a/integration/python/python_tasks_test.go b/integration/python/python_tasks_test.go index 9ad3ed5de..9411afb13 100644 --- a/integration/python/python_tasks_test.go +++ b/integration/python/python_tasks_test.go @@ -266,7 +266,7 @@ func prepareRepoFiles(t *testing.T) *testFiles { func GenerateNotebookTasks(notebookPath string, versions []string, nodeTypeId string) []jobs.SubmitTask { tasks := make([]jobs.SubmitTask, 0) - for i := 0; i < len(versions); i++ { + for i := range versions { task := jobs.SubmitTask{ TaskKey: fmt.Sprintf("notebook_%s", strings.ReplaceAll(versions[i], ".", "_")), NotebookTask: &jobs.NotebookTask{ @@ -287,7 +287,7 @@ func GenerateNotebookTasks(notebookPath string, versions []string, nodeTypeId st func GenerateSparkPythonTasks(notebookPath string, versions []string, nodeTypeId string) []jobs.SubmitTask { tasks := make([]jobs.SubmitTask, 0) - for i := 0; i < len(versions); i++ { + for i := range versions { task := jobs.SubmitTask{ TaskKey: fmt.Sprintf("spark_%s", strings.ReplaceAll(versions[i], ".", "_")), SparkPythonTask: &jobs.SparkPythonTask{ @@ -308,7 +308,7 @@ func GenerateSparkPythonTasks(notebookPath string, versions []string, nodeTypeId func GenerateWheelTasks(wheelPath string, versions []string, nodeTypeId string) []jobs.SubmitTask { tasks := make([]jobs.SubmitTask, 0) - for i := 0; i < len(versions); i++ { + for i := range versions { task := jobs.SubmitTask{ TaskKey: fmt.Sprintf("whl_%s", strings.ReplaceAll(versions[i], ".", "_")), PythonWheelTask: &jobs.PythonWheelTask{ diff --git a/libs/cmdio/render.go b/libs/cmdio/render.go index 1529274a3..1a6aadcfa 100644 --- a/libs/cmdio/render.go +++ b/libs/cmdio/render.go @@ -39,7 +39,7 @@ func Heredoc(tmpl string) (trimmed string) { break } } - for i := 0; i < len(lines); i++ { + for i := range lines { if lines[i] == "" || strings.TrimSpace(lines[i]) == "" { continue } diff --git a/libs/cmdio/render_test.go b/libs/cmdio/render_test.go index f26190a23..1c9d1c93b 100644 --- a/libs/cmdio/render_test.go +++ b/libs/cmdio/render_test.go @@ -55,7 +55,7 @@ func (d *dummyIterator) Next(ctx context.Context) (*provisioning.Workspace, erro func makeWorkspaces(count int) []*provisioning.Workspace { res := make([]*provisioning.Workspace, 0, count) next := []*provisioning.Workspace{&dummyWorkspace1, &dummyWorkspace2} - for i := 0; i < count; i++ { + for range count { n := next[0] next = append(next[1:], n) res = append(res, n) diff --git a/libs/dyn/convert/from_typed.go b/libs/dyn/convert/from_typed.go index ed1b85a36..3de017b75 100644 --- a/libs/dyn/convert/from_typed.go +++ b/libs/dyn/convert/from_typed.go @@ -209,7 +209,7 @@ func fromTypedSlice(src reflect.Value, ref dyn.Value) (dyn.Value, error) { } out := make([]dyn.Value, src.Len()) - for i := 0; i < src.Len(); i++ { + for i := range src.Len() { v := src.Index(i) refv := ref.Index(i) diff --git a/libs/dyn/convert/struct_info.go b/libs/dyn/convert/struct_info.go index f5fd29cb9..1e34008e2 100644 --- a/libs/dyn/convert/struct_info.go +++ b/libs/dyn/convert/struct_info.go @@ -65,7 +65,7 @@ func buildStructInfo(typ reflect.Type) structInfo { } nf := styp.NumField() - for j := 0; j < nf; j++ { + for j := range nf { sf := styp.Field(j) // Recurse into anonymous fields. diff --git a/libs/dyn/mapping_test.go b/libs/dyn/mapping_test.go index 43b24b0c5..67144ae55 100644 --- a/libs/dyn/mapping_test.go +++ b/libs/dyn/mapping_test.go @@ -185,7 +185,7 @@ func TestMappingClone(t *testing.T) { func TestMappingMerge(t *testing.T) { var m1 dyn.Mapping - for i := 0; i < 10; i++ { + for i := range 10 { err := m1.Set(dyn.V(fmt.Sprintf("%d", i)), dyn.V(i)) require.NoError(t, err) } diff --git a/libs/dyn/merge/override.go b/libs/dyn/merge/override.go index ca62c7305..1e49d5544 100644 --- a/libs/dyn/merge/override.go +++ b/libs/dyn/merge/override.go @@ -165,7 +165,7 @@ func overrideSequence(basePath dyn.Path, left, right []dyn.Value, visitor Overri minLen := min(len(left), len(right)) var values []dyn.Value - for i := 0; i < minLen; i++ { + for i := range minLen { path := basePath.Append(dyn.Index(i)) merged, err := override(path, left[i], right[i], visitor) if err != nil { diff --git a/libs/dyn/path.go b/libs/dyn/path.go index 76377e2dc..25bff5070 100644 --- a/libs/dyn/path.go +++ b/libs/dyn/path.go @@ -65,7 +65,7 @@ func (p Path) Equal(q Path) bool { if pl != ql { return false } - for i := 0; i < pl; i++ { + for i := range pl { if p[i] != q[i] { return false } @@ -81,7 +81,7 @@ func (p Path) HasPrefix(q Path) bool { if pl < ql { return false } - for i := 0; i < ql; i++ { + for i := range ql { if p[i] != q[i] { return false } diff --git a/libs/dyn/visit_map_test.go b/libs/dyn/visit_map_test.go index d62327d6f..3c2908c4b 100644 --- a/libs/dyn/visit_map_test.go +++ b/libs/dyn/visit_map_test.go @@ -87,12 +87,12 @@ func TestMapFuncOnMapWithEmptySequence(t *testing.T) { dyn.V([]dyn.Value{dyn.V(42)}), } - for i := 0; i < len(variants); i++ { + for i := range variants { vin := dyn.V(map[string]dyn.Value{ "key": variants[i], }) - for j := 0; j < len(variants); j++ { + for j := range variants { vout, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Key("key")), func(_ dyn.Path, v dyn.Value) (dyn.Value, error) { return variants[j], nil }) @@ -153,12 +153,12 @@ func TestMapFuncOnSequenceWithEmptySequence(t *testing.T) { dyn.V([]dyn.Value{dyn.V(42)}), } - for i := 0; i < len(variants); i++ { + for i := range variants { vin := dyn.V([]dyn.Value{ variants[i], }) - for j := 0; j < len(variants); j++ { + for j := range variants { vout, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Index(0)), func(_ dyn.Path, v dyn.Value) (dyn.Value, error) { return variants[j], nil }) diff --git a/libs/exec/exec_test.go b/libs/exec/exec_test.go index e75c158bd..c363c1f7c 100644 --- a/libs/exec/exec_test.go +++ b/libs/exec/exec_test.go @@ -141,7 +141,7 @@ func TestMultipleCommandsRunInParrallel(t *testing.T) { const count = 5 var wg sync.WaitGroup - for i := 0; i < count; i++ { + for i := range count { wg.Add(1) cmd, err := executor.StartCommand(context.Background(), fmt.Sprintf("echo 'Hello %d'", i)) go func(cmd Command, i int) { diff --git a/libs/filer/fs_test.go b/libs/filer/fs_test.go index 08d7a9428..6168af39a 100644 --- a/libs/filer/fs_test.go +++ b/libs/filer/fs_test.go @@ -107,7 +107,7 @@ func TestFsOpenDir(t *testing.T) { de.Close() - for i := 0; i < 3; i++ { + for range 3 { tmp, err = de.ReadDir(1) require.NoError(t, err) entries = append(entries, tmp...) diff --git a/libs/jsonschema/from_type.go b/libs/jsonschema/from_type.go index 18a2b3ba5..6f8f39d96 100644 --- a/libs/jsonschema/from_type.go +++ b/libs/jsonschema/from_type.go @@ -211,7 +211,7 @@ func getStructFields(typ reflect.Type) []reflect.StructField { fields := []reflect.StructField{} bfsQueue := list.New() - for i := 0; i < typ.NumField(); i++ { + for i := range typ.NumField() { bfsQueue.PushBack(typ.Field(i)) } for bfsQueue.Len() > 0 { @@ -233,7 +233,7 @@ func getStructFields(typ reflect.Type) []reflect.StructField { fieldType = fieldType.Elem() } - for i := 0; i < fieldType.NumField(); i++ { + for i := range fieldType.NumField() { bfsQueue.PushBack(fieldType.Field(i)) } } From 8e8399da8312c127bcfc3760efafc60d1a501214 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 3 Jan 2025 11:13:12 +0100 Subject: [PATCH 22/53] Enable linter 'mirror' and autofix existing issues (#2070) https://github.com/butuzov/mirror --- .golangci.yaml | 1 + bundle/deploy/terraform/import.go | 3 ++- libs/cmdio/render_test.go | 2 +- libs/filer/workspace_files_client.go | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 6558c502d..06d8152e5 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -13,6 +13,7 @@ linters: - goimports - testifylint - intrange + - mirror linters-settings: govet: enable-all: true diff --git a/bundle/deploy/terraform/import.go b/bundle/deploy/terraform/import.go index 0a1d1b9ce..a0604e71d 100644 --- a/bundle/deploy/terraform/import.go +++ b/bundle/deploy/terraform/import.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/libs/cmdio" @@ -67,7 +68,7 @@ func (m *importResource) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn if changed && !m.opts.AutoApprove { output := buf.String() // Remove output starting from Warning until end of output - output = output[:bytes.Index([]byte(output), []byte("Warning:"))] + output = output[:strings.Index(output, "Warning:")] cmdio.LogString(ctx, output) if !cmdio.IsPromptSupported(ctx) { diff --git a/libs/cmdio/render_test.go b/libs/cmdio/render_test.go index 1c9d1c93b..51b385b1d 100644 --- a/libs/cmdio/render_test.go +++ b/libs/cmdio/render_test.go @@ -74,7 +74,7 @@ func makeIterator(count int) listing.Iterator[*provisioning.Workspace] { func makeBigOutput(count int) string { res := bytes.Buffer{} for _, ws := range makeWorkspaces(count) { - res.Write([]byte(fmt.Sprintf("%d %s\n", ws.WorkspaceId, ws.WorkspaceName))) + res.WriteString(fmt.Sprintf("%d %s\n", ws.WorkspaceId, ws.WorkspaceName)) } return res.String() } diff --git a/libs/filer/workspace_files_client.go b/libs/filer/workspace_files_client.go index 9e0a7ce50..8d5148edd 100644 --- a/libs/filer/workspace_files_client.go +++ b/libs/filer/workspace_files_client.go @@ -195,7 +195,7 @@ func (w *WorkspaceFilesClient) Write(ctx context.Context, name string, reader io // This API returns 400 if the file already exists, when the object type is notebook regex := regexp.MustCompile(`Path \((.*)\) already exists.`) - if aerr.StatusCode == http.StatusBadRequest && regex.Match([]byte(aerr.Message)) { + if aerr.StatusCode == http.StatusBadRequest && regex.MatchString(aerr.Message) { // Parse file path from regex capture group matches := regex.FindStringSubmatch(aerr.Message) if len(matches) == 2 { From 9abb11decbc9e2293d5e4b3c6a4eabb7ffeca21c Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:05:20 +0530 Subject: [PATCH 23/53] Enable TestBundleInitOnMlopsStacks (#2072) ## Changes This test was broken due to upstream change: https://github.com/databricks/mlops-stacks/pull/187 Fixed in upstream change: https://github.com/databricks/mlops-stacks/pull/188 ## Tests Test passes now --- integration/bundle/init_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration/bundle/init_test.go b/integration/bundle/init_test.go index bc81cd3a8..f5c263ca3 100644 --- a/integration/bundle/init_test.go +++ b/integration/bundle/init_test.go @@ -39,8 +39,6 @@ func TestBundleInitErrorOnUnknownFields(t *testing.T) { // make changes that can break the MLOps Stacks DAB. In which case we should // skip this test until the MLOps Stacks DAB is updated to work again. func TestBundleInitOnMlopsStacks(t *testing.T) { - testutil.SkipUntil(t, "2025-01-09") - ctx, wt := acc.WorkspaceTest(t) w := wt.W From ab6b1f1d772fa84adc6fd8cab954fbfd5f38a1ba Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 3 Jan 2025 12:32:03 +0100 Subject: [PATCH 24/53] Upgrade golang.org/x/net from v0.26.0 to v0.33.0 (#2073) Fixes https://github.com/databricks/cli/security/dependabot/20 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2dda0cd60..86bc1c368 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/api v0.182.0 // indirect diff --git a/go.sum b/go.sum index 1e806ea03..f6cf79607 100644 --- a/go.sum +++ b/go.sum @@ -204,8 +204,8 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= From 4f3cf6ac2cad002ac3e6d24dbda657b4c22fb314 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Fri, 3 Jan 2025 15:12:55 +0100 Subject: [PATCH 25/53] Limit comment about integration tests to PRs from forks (#2075) ## Changes The comment block appears on all PRs, even if the integration tests are automatically triggered. This is quite noisy. This change limits those comments to PRs from forks. ## Tests Have to try by merging... --- .github/workflows/external-message.yml | 6 +++++- .github/workflows/integration-pr.yml | 25 +++---------------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/.github/workflows/external-message.yml b/.github/workflows/external-message.yml index 1970735f9..28e61503c 100644 --- a/.github/workflows/external-message.yml +++ b/.github/workflows/external-message.yml @@ -17,6 +17,10 @@ jobs: permissions: pull-requests: write + # Only run this job for PRs from forks. + # Integration tests are not run automatically for PRs from forks. + if: "${{ github.event.pull_request.head.repo.fork }}" + steps: - uses: actions/checkout@v4 @@ -43,7 +47,7 @@ jobs: run: | gh pr comment ${{ github.event.pull_request.number }} --body \ " - If integration tests don't run automatically, an authorized user can run them manually by following the instructions below: + An authorized user can trigger integration tests manually by following the instructions below: Trigger: [go/deco-tests-run/cli](https://go/deco-tests-run/cli) diff --git a/.github/workflows/integration-pr.yml b/.github/workflows/integration-pr.yml index bf2dcd8bc..c1e3a9a29 100644 --- a/.github/workflows/integration-pr.yml +++ b/.github/workflows/integration-pr.yml @@ -5,36 +5,17 @@ on: types: [opened, synchronize] jobs: - check-token: - runs-on: ubuntu-latest - environment: "test-trigger-is" - - outputs: - has_token: ${{ steps.set-token-status.outputs.has_token }} - - steps: - - name: Check if DECO_WORKFLOW_TRIGGER_APP_ID is set - id: set-token-status - run: | - if [ -z "${{ secrets.DECO_WORKFLOW_TRIGGER_APP_ID }}" ]; then - echo "DECO_WORKFLOW_TRIGGER_APP_ID is empty. User has no access to secrets." - echo "::set-output name=has_token::false" - else - echo "DECO_WORKFLOW_TRIGGER_APP_ID is set. User has access to secrets." - echo "::set-output name=has_token::true" - fi - # Trigger for pull requests. # # This workflow triggers the integration test workflow in a different repository. # It requires secrets from the "test-trigger-is" environment, which are only available to authorized users. - # It depends on the "check-token" workflow to confirm access to this environment to avoid failures. trigger: runs-on: ubuntu-latest environment: "test-trigger-is" - if: needs.check-token.outputs.has_token == 'true' - needs: check-token + # Only run this job for PRs from branches on the main repository and not from forks. + # Workflows triggered by PRs from forks don't have access to the "test-trigger-is" environment. + if: "${{ !github.event.pull_request.head.repo.fork }}" steps: - name: Generate GitHub App Token From 8af98accee855eb336a8f658d6d63e6de278e132 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 6 Jan 2025 14:12:34 +0100 Subject: [PATCH 26/53] Set gotestsum --format to github-actions when running on github (#2082) This gives grouping and ability to open individual test logs. --- .github/workflows/push.yml | 3 +++ Makefile | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f7c4d5456..a4a35420a 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -14,6 +14,9 @@ on: branches: - main +env: + GOTESTSUM_FORMAT: github-actions + jobs: tests: runs-on: ${{ matrix.os }} diff --git a/Makefile b/Makefile index b6a62765f..f8b725900 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ default: build PACKAGES=./libs/... ./internal/... ./cmd/... ./bundle/... . +GOTESTSUM_FORMAT ?= pkgname-and-test-fails + lint: ./lint.sh ./... @@ -9,10 +11,10 @@ lintcheck: golangci-lint run ./... test: - gotestsum --format pkgname-and-test-fails --no-summary=skipped -- ${PACKAGES} + gotestsum --format ${GOTESTSUM_FORMAT} --no-summary=skipped -- ${PACKAGES} cover: - gotestsum --format pkgname-and-test-fails --no-summary=skipped -- -coverprofile=coverage.txt ${PACKAGES} + gotestsum --format ${GOTESTSUM_FORMAT} --no-summary=skipped -- -coverprofile=coverage.txt ${PACKAGES} showcover: go tool cover -html=coverage.txt From 31552852ff6856a70f54454322d7407f6fdcfb06 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Mon, 6 Jan 2025 15:30:48 +0100 Subject: [PATCH 27/53] Disable integration workflows (#2085) See https://github.com/databricks/cli/issues/2084. --- .github/workflows/external-message.yml | 12 ++++++++---- .github/workflows/integration-approve.yml | 6 +++++- .github/workflows/integration-main.yml | 10 +++++++--- .github/workflows/integration-pr.yml | 8 ++++++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/external-message.yml b/.github/workflows/external-message.yml index 28e61503c..9c91242a7 100644 --- a/.github/workflows/external-message.yml +++ b/.github/workflows/external-message.yml @@ -6,10 +6,14 @@ name: PR Comment # DO NOT PULL THE PR OR EXECUTE ANY CODE FROM THE PR. on: - pull_request_target: - types: [opened, reopened, synchronize] - branches: - - main + workflow_dispatch: + + # Disable because of https://github.com/databricks/cli/issues/2084. + # + # pull_request_target: + # types: [opened, reopened, synchronize] + # branches: + # - main jobs: comment-on-pr: diff --git a/.github/workflows/integration-approve.yml b/.github/workflows/integration-approve.yml index 4bdeb62a3..265574bb5 100644 --- a/.github/workflows/integration-approve.yml +++ b/.github/workflows/integration-approve.yml @@ -1,7 +1,11 @@ name: integration-approve on: - merge_group: + workflow_dispatch: + + # Disable because of https://github.com/databricks/cli/issues/2084. + # + # merge_group: jobs: # Trigger for merge groups. diff --git a/.github/workflows/integration-main.yml b/.github/workflows/integration-main.yml index 064e439cf..55fc98ae4 100644 --- a/.github/workflows/integration-main.yml +++ b/.github/workflows/integration-main.yml @@ -1,9 +1,13 @@ name: integration-main on: - push: - branches: - - main + workflow_dispatch: + + # Disable because of https://github.com/databricks/cli/issues/2084. + # + # push: + # branches: + # - main jobs: # Trigger for pushes to the main branch. diff --git a/.github/workflows/integration-pr.yml b/.github/workflows/integration-pr.yml index c1e3a9a29..965d74fbe 100644 --- a/.github/workflows/integration-pr.yml +++ b/.github/workflows/integration-pr.yml @@ -1,8 +1,12 @@ name: integration-pr on: - pull_request: - types: [opened, synchronize] + workflow_dispatch: + + # Disable because of https://github.com/databricks/cli/issues/2084. + # + # pull_request: + # types: [opened, synchronize] jobs: # Trigger for pull requests. From c262b30ef4a28685ef5e52c9d4985d0fe0591382 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Mon, 6 Jan 2025 16:34:42 +0100 Subject: [PATCH 28/53] Migrate workflows that need write access to use hosted runners (#2077) ## Changes Migrate workflows to Databricks-hosted GitHub Actions runners. The GitHub-hosted runners can no longer be used because of security hardening. --- .github/workflows/close-stale-issues.yml | 8 ++++++-- .github/workflows/external-message.yml | 5 ++++- .github/workflows/integration-approve.yml | 4 +++- .github/workflows/integration-main.yml | 5 ++++- .github/workflows/integration-pr.yml | 5 ++++- .github/workflows/release-snapshot.yml | 5 ++++- .github/workflows/release.yml | 6 +++++- 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml index ffe550132..273b89a9c 100644 --- a/.github/workflows/close-stale-issues.yml +++ b/.github/workflows/close-stale-issues.yml @@ -7,12 +7,16 @@ on: jobs: cleanup: + name: Stale issue job + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + permissions: issues: write contents: read pull-requests: write - runs-on: ubuntu-latest - name: Stale issue job + steps: - uses: actions/stale@v9 with: diff --git a/.github/workflows/external-message.yml b/.github/workflows/external-message.yml index 9c91242a7..eb68a36e4 100644 --- a/.github/workflows/external-message.yml +++ b/.github/workflows/external-message.yml @@ -17,7 +17,10 @@ on: jobs: comment-on-pr: - runs-on: ubuntu-latest + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + permissions: pull-requests: write diff --git a/.github/workflows/integration-approve.yml b/.github/workflows/integration-approve.yml index 265574bb5..0f6b209cb 100644 --- a/.github/workflows/integration-approve.yml +++ b/.github/workflows/integration-approve.yml @@ -21,7 +21,9 @@ jobs: # * Avoid running integration tests twice, since it was already run at the tip of the branch before squashing. # trigger: - runs-on: ubuntu-latest + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco steps: - name: Auto-approve squashed commit diff --git a/.github/workflows/integration-main.yml b/.github/workflows/integration-main.yml index 55fc98ae4..5a78d4fd8 100644 --- a/.github/workflows/integration-main.yml +++ b/.github/workflows/integration-main.yml @@ -15,7 +15,10 @@ jobs: # This workflow triggers the integration test workflow in a different repository. # It requires secrets from the "test-trigger-is" environment, which are only available to authorized users. trigger: - runs-on: ubuntu-latest + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + environment: "test-trigger-is" steps: diff --git a/.github/workflows/integration-pr.yml b/.github/workflows/integration-pr.yml index 965d74fbe..fd170f77e 100644 --- a/.github/workflows/integration-pr.yml +++ b/.github/workflows/integration-pr.yml @@ -14,7 +14,10 @@ jobs: # This workflow triggers the integration test workflow in a different repository. # It requires secrets from the "test-trigger-is" environment, which are only available to authorized users. trigger: - runs-on: ubuntu-latest + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + environment: "test-trigger-is" # Only run this job for PRs from branches on the main repository and not from forks. diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml index 7ef8b43c9..5c56a294e 100644 --- a/.github/workflows/release-snapshot.yml +++ b/.github/workflows/release-snapshot.yml @@ -20,7 +20,10 @@ on: jobs: goreleaser: - runs-on: ubuntu-latest + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + steps: - name: Checkout repository and submodules uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4a253531..88e338a8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,9 +9,13 @@ on: jobs: goreleaser: + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + outputs: artifacts: ${{ steps.releaser.outputs.artifacts }} - runs-on: ubuntu-latest + steps: - name: Checkout repository and submodules uses: actions/checkout@v4 From 3629c9e4062f0de58c7c8ce9524e050621be3cb7 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Mon, 6 Jan 2025 17:07:15 +0100 Subject: [PATCH 29/53] Enable integration workflows (#2086) ## Changes This reverts commit 31552852ff6856a70f54454322d7407f6fdcfb06. These workflows were disabled in #2085. They should work again now that we're using self-hosted runners (see #2077). ## Tests (inline) --- .github/workflows/external-message.yml | 12 ++++-------- .github/workflows/integration-approve.yml | 6 +----- .github/workflows/integration-main.yml | 10 +++------- .github/workflows/integration-pr.yml | 8 ++------ 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/.github/workflows/external-message.yml b/.github/workflows/external-message.yml index eb68a36e4..f06d81a47 100644 --- a/.github/workflows/external-message.yml +++ b/.github/workflows/external-message.yml @@ -6,14 +6,10 @@ name: PR Comment # DO NOT PULL THE PR OR EXECUTE ANY CODE FROM THE PR. on: - workflow_dispatch: - - # Disable because of https://github.com/databricks/cli/issues/2084. - # - # pull_request_target: - # types: [opened, reopened, synchronize] - # branches: - # - main + pull_request_target: + types: [opened, reopened, synchronize] + branches: + - main jobs: comment-on-pr: diff --git a/.github/workflows/integration-approve.yml b/.github/workflows/integration-approve.yml index 0f6b209cb..293d31a2a 100644 --- a/.github/workflows/integration-approve.yml +++ b/.github/workflows/integration-approve.yml @@ -1,11 +1,7 @@ name: integration-approve on: - workflow_dispatch: - - # Disable because of https://github.com/databricks/cli/issues/2084. - # - # merge_group: + merge_group: jobs: # Trigger for merge groups. diff --git a/.github/workflows/integration-main.yml b/.github/workflows/integration-main.yml index 5a78d4fd8..0b6032d50 100644 --- a/.github/workflows/integration-main.yml +++ b/.github/workflows/integration-main.yml @@ -1,13 +1,9 @@ name: integration-main on: - workflow_dispatch: - - # Disable because of https://github.com/databricks/cli/issues/2084. - # - # push: - # branches: - # - main + push: + branches: + - main jobs: # Trigger for pushes to the main branch. diff --git a/.github/workflows/integration-pr.yml b/.github/workflows/integration-pr.yml index fd170f77e..0f9c4797a 100644 --- a/.github/workflows/integration-pr.yml +++ b/.github/workflows/integration-pr.yml @@ -1,12 +1,8 @@ name: integration-pr on: - workflow_dispatch: - - # Disable because of https://github.com/databricks/cli/issues/2084. - # - # pull_request: - # types: [opened, synchronize] + pull_request: + types: [opened, synchronize] jobs: # Trigger for pull requests. From e2cd8c2f34057c9d1f954f454d1a4d29bf656deb Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 7 Jan 2025 11:49:23 +0100 Subject: [PATCH 30/53] Enable perfsprint linter and apply autofix (#2071) https://github.com/catenacyber/perfsprint --- .golangci.yaml | 1 + bundle/artifacts/expand_globs_test.go | 9 ++++---- bundle/artifacts/whl/infer.go | 2 +- bundle/bundle.go | 3 ++- bundle/config/artifact.go | 4 ++-- .../config/mutator/expand_workspace_root.go | 3 +-- .../mutator/prepend_workspace_prefix.go | 2 +- .../mutator/python/python_mutator_test.go | 23 ++++++++++--------- .../mutator/resolve_variable_references.go | 6 ++--- bundle/config/resources/clusters.go | 3 +-- bundle/config/resources/job.go | 3 +-- bundle/config/resources/mlflow_experiment.go | 3 +-- bundle/config/resources/mlflow_model.go | 3 +-- .../resources/model_serving_endpoint.go | 3 +-- bundle/config/resources/permission.go | 2 +- bundle/config/resources/pipeline.go | 3 +-- bundle/config/resources/quality_monitor.go | 3 +-- bundle/config/resources/registered_model.go | 3 +-- bundle/config/resources/schema.go | 6 ++--- bundle/config/resources/volume.go | 6 ++--- bundle/config/validate/folder_permissions.go | 3 ++- .../config/validate/unique_resource_keys.go | 3 +-- .../config/validate/validate_artifact_path.go | 2 +- .../validate/validate_artifact_path_test.go | 5 ++-- bundle/config/variable/lookup.go | 6 ++--- bundle/config/variable/resolve_alert.go | 5 ++-- bundle/config/variable/resolve_cluster.go | 2 +- .../config/variable/resolve_cluster_policy.go | 5 ++-- bundle/config/variable/resolve_dashboard.go | 5 ++-- .../config/variable/resolve_instance_pool.go | 5 ++-- bundle/config/variable/resolve_job.go | 6 ++--- bundle/config/variable/resolve_metastore.go | 5 ++-- .../resolve_notification_destination.go | 2 +- .../resolve_notification_destination_test.go | 4 ++-- bundle/config/variable/resolve_pipeline.go | 5 ++-- bundle/config/variable/resolve_query.go | 5 ++-- .../variable/resolve_service_principal.go | 5 ++-- bundle/config/variable/resolve_warehouse.go | 5 ++-- bundle/config/variable/variable.go | 3 ++- bundle/deploy/state.go | 3 ++- .../check_dashboards_modified_remotely.go | 2 +- ...check_dashboards_modified_remotely_test.go | 4 ++-- bundle/deploy/terraform/init.go | 2 +- bundle/deploy/terraform/load.go | 3 ++- bundle/libraries/helpers.go | 4 ++-- bundle/permissions/workspace_root.go | 3 ++- bundle/phases/deploy.go | 6 ++--- bundle/phases/destroy.go | 5 ++-- bundle/root.go | 3 ++- bundle/run/job.go | 7 +++--- bundle/run/job_options.go | 10 ++++---- bundle/run/output/job.go | 2 +- bundle/run/output/task.go | 9 ++++---- bundle/run/pipeline.go | 9 ++++---- bundle/run/progress/pipeline.go | 2 +- bundle/tests/run_as_test.go | 3 +-- bundle/trampoline/python_wheel.go | 3 ++- bundle/trampoline/trampoline_test.go | 4 ++-- cmd/auth/auth.go | 6 ++--- cmd/auth/describe_test.go | 6 ++--- cmd/auth/env.go | 4 ++-- cmd/auth/login.go | 2 +- cmd/bundle/destroy.go | 8 +++---- cmd/bundle/generate/dashboard.go | 6 ++--- cmd/bundle/generate/job.go | 6 ++--- cmd/bundle/generate/pipeline.go | 6 ++--- cmd/bundle/generate/utils.go | 2 +- cmd/bundle/launch.go | 4 ++-- cmd/bundle/open.go | 4 ++-- cmd/bundle/run.go | 3 ++- cmd/bundle/test.go | 4 ++-- cmd/bundle/validate.go | 3 ++- cmd/configure/configure.go | 5 ++-- cmd/configure/host.go | 6 ++--- cmd/labs/github/repositories.go | 2 +- cmd/labs/installed.go | 3 ++- cmd/labs/localcache/jsonfile.go | 2 +- cmd/labs/localcache/jsonfile_test.go | 3 +-- cmd/labs/project/installer.go | 12 +++++----- cmd/labs/show.go | 4 ++-- cmd/root/auth.go | 8 +++---- cmd/root/progress_logger.go | 4 ++-- cmd/sync/completion.go | 5 ++-- cmd/sync/sync.go | 3 ++- cmd/workspace/repos/overrides.go | 3 ++- cmd/workspace/secrets/put_secret.go | 4 ++-- cmd/workspace/workspace/overrides.go | 2 +- integration/bundle/bind_resource_test.go | 16 ++++++------- integration/bundle/clusters_test.go | 5 ++-- integration/bundle/dashboards_test.go | 2 +- integration/bundle/deploy_to_shared_test.go | 3 +-- integration/bundle/empty_bundle_test.go | 4 +--- integration/bundle/generate_job_test.go | 6 ++--- integration/bundle/generate_pipeline_test.go | 7 +++--- integration/bundle/init_test.go | 2 +- integration/cmd/auth/describe_test.go | 11 ++++----- integration/cmd/fs/completion_test.go | 3 +-- integration/cmd/jobs/jobs_test.go | 4 ++-- integration/cmd/sync/sync_test.go | 5 +--- integration/cmd/workspace/workspace_test.go | 4 ++-- integration/internal/acc/fixtures.go | 2 +- integration/python/python_tasks_test.go | 11 ++++----- libs/auth/oauth.go | 12 +++++----- libs/auth/oauth_test.go | 4 ++-- libs/cmdio/error_event.go | 4 +--- libs/cmdio/logger.go | 7 +++--- libs/databrickscfg/cfgpickers/clusters.go | 2 +- libs/databrickscfg/loader.go | 2 +- libs/dyn/convert/normalize.go | 2 +- libs/dyn/jsonloader/json.go | 5 ++-- libs/dyn/location.go | 3 ++- libs/dyn/mapping_test.go | 6 ++--- libs/dyn/merge/override_test.go | 4 ++-- libs/dyn/path_string_test.go | 18 +++++++-------- libs/dyn/visit_map_test.go | 9 ++++---- libs/dyn/yamlsaver/saver.go | 4 ++-- libs/errs/aggregate_test.go | 12 +++++----- libs/exec/shell_cmd.go | 3 +-- libs/fakefs/fakefs.go | 4 ++-- libs/filer/fake_filer.go | 8 +++---- libs/filer/filer.go | 15 ++++++------ libs/filer/files_client.go | 5 +--- libs/filer/workspace_files_cache_test.go | 4 ++-- libs/flags/json_flag_test.go | 3 +-- libs/flags/output.go | 3 ++- libs/git/reference.go | 4 ++-- libs/git/repository_test.go | 4 +--- libs/git/worktree_test.go | 6 ++--- libs/jsonschema/instance.go | 5 ++-- libs/jsonschema/utils.go | 2 +- libs/locker/locker.go | 6 ++--- libs/process/background_test.go | 4 ++-- libs/process/stub.go | 2 +- libs/process/stub_test.go | 8 +++---- libs/sync/event.go | 10 ++++---- libs/sync/path.go | 2 +- libs/sync/sync.go | 3 ++- libs/tags/tag.go | 7 +++--- libs/template/config.go | 4 ++-- libs/template/materialize_test.go | 3 +-- libs/template/renderer_test.go | 3 +-- 141 files changed, 330 insertions(+), 353 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 06d8152e5..07a6afdc5 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -14,6 +14,7 @@ linters: - testifylint - intrange - mirror + - perfsprint linters-settings: govet: enable-all: true diff --git a/bundle/artifacts/expand_globs_test.go b/bundle/artifacts/expand_globs_test.go index dc7c77de7..264c52c50 100644 --- a/bundle/artifacts/expand_globs_test.go +++ b/bundle/artifacts/expand_globs_test.go @@ -2,7 +2,6 @@ package artifacts import ( "context" - "fmt" "path/filepath" "testing" @@ -88,16 +87,16 @@ func TestExpandGlobs_InvalidPattern(t *testing.T) { )) assert.Len(t, diags, 4) - assert.Equal(t, fmt.Sprintf("%s: syntax error in pattern", filepath.Clean("a[.txt")), diags[0].Summary) + assert.Equal(t, filepath.Clean("a[.txt")+": syntax error in pattern", diags[0].Summary) assert.Equal(t, filepath.Join(tmpDir, "databricks.yml"), diags[0].Locations[0].File) assert.Equal(t, "artifacts.test.files[0].source", diags[0].Paths[0].String()) - assert.Equal(t, fmt.Sprintf("%s: syntax error in pattern", filepath.Clean("a[.txt")), diags[1].Summary) + assert.Equal(t, filepath.Clean("a[.txt")+": syntax error in pattern", diags[1].Summary) assert.Equal(t, filepath.Join(tmpDir, "databricks.yml"), diags[1].Locations[0].File) assert.Equal(t, "artifacts.test.files[1].source", diags[1].Paths[0].String()) - assert.Equal(t, fmt.Sprintf("%s: syntax error in pattern", filepath.Clean("../a[.txt")), diags[2].Summary) + assert.Equal(t, filepath.Clean("../a[.txt")+": syntax error in pattern", diags[2].Summary) assert.Equal(t, filepath.Join(tmpDir, "databricks.yml"), diags[2].Locations[0].File) assert.Equal(t, "artifacts.test.files[2].source", diags[2].Paths[0].String()) - assert.Equal(t, fmt.Sprintf("%s: syntax error in pattern", filepath.Clean("subdir/a[.txt")), diags[3].Summary) + assert.Equal(t, filepath.Clean("subdir/a[.txt")+": syntax error in pattern", diags[3].Summary) assert.Equal(t, filepath.Join(tmpDir, "databricks.yml"), diags[3].Locations[0].File) assert.Equal(t, "artifacts.test.files[3].source", diags[3].Paths[0].String()) } diff --git a/bundle/artifacts/whl/infer.go b/bundle/artifacts/whl/infer.go index 604bfc449..9c40360be 100644 --- a/bundle/artifacts/whl/infer.go +++ b/bundle/artifacts/whl/infer.go @@ -32,7 +32,7 @@ func (m *infer) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { //) py := python.GetExecutable() - artifact.BuildCommand = fmt.Sprintf(`%s setup.py bdist_wheel`, py) + artifact.BuildCommand = py + " setup.py bdist_wheel" return nil } diff --git a/bundle/bundle.go b/bundle/bundle.go index 573bcef2f..1f5e2a294 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -8,6 +8,7 @@ package bundle import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -234,7 +235,7 @@ func (b *Bundle) GetSyncIncludePatterns(ctx context.Context) ([]string, error) { // we call into from this bundle context. func (b *Bundle) AuthEnv() (map[string]string, error) { if b.client == nil { - return nil, fmt.Errorf("workspace client not initialized yet") + return nil, errors.New("workspace client not initialized yet") } cfg := b.client.Config diff --git a/bundle/config/artifact.go b/bundle/config/artifact.go index 9a5690f57..177799e11 100644 --- a/bundle/config/artifact.go +++ b/bundle/config/artifact.go @@ -2,7 +2,7 @@ package config import ( "context" - "fmt" + "errors" "github.com/databricks/cli/libs/exec" ) @@ -37,7 +37,7 @@ type Artifact struct { func (a *Artifact) Build(ctx context.Context) ([]byte, error) { if a.BuildCommand == "" { - return nil, fmt.Errorf("no build property defined") + return nil, errors.New("no build property defined") } var e *exec.Executor diff --git a/bundle/config/mutator/expand_workspace_root.go b/bundle/config/mutator/expand_workspace_root.go index a29d129b0..2ec70548f 100644 --- a/bundle/config/mutator/expand_workspace_root.go +++ b/bundle/config/mutator/expand_workspace_root.go @@ -2,7 +2,6 @@ package mutator import ( "context" - "fmt" "path" "strings" @@ -33,7 +32,7 @@ func (m *expandWorkspaceRoot) Apply(ctx context.Context, b *bundle.Bundle) diag. } if strings.HasPrefix(root, "~/") { - home := fmt.Sprintf("/Workspace/Users/%s", currentUser.UserName) + home := "/Workspace/Users/" + currentUser.UserName b.Config.Workspace.RootPath = path.Join(home, root[2:]) } diff --git a/bundle/config/mutator/prepend_workspace_prefix.go b/bundle/config/mutator/prepend_workspace_prefix.go index b093ec26a..616759ee4 100644 --- a/bundle/config/mutator/prepend_workspace_prefix.go +++ b/bundle/config/mutator/prepend_workspace_prefix.go @@ -55,7 +55,7 @@ func (m *prependWorkspacePrefix) Apply(ctx context.Context, b *bundle.Bundle) di } } - return dyn.NewValue(fmt.Sprintf("/Workspace%s", path), v.Locations()), nil + return dyn.NewValue("/Workspace"+path, v.Locations()), nil }) if err != nil { return dyn.InvalidValue, err diff --git a/bundle/config/mutator/python/python_mutator_test.go b/bundle/config/mutator/python/python_mutator_test.go index 8bdf91d03..ff21f8ed9 100644 --- a/bundle/config/mutator/python/python_mutator_test.go +++ b/bundle/config/mutator/python/python_mutator_test.go @@ -2,6 +2,7 @@ package python import ( "context" + "errors" "fmt" "os" "os/exec" @@ -319,15 +320,15 @@ func TestCreateOverrideVisitor(t *testing.T) { updatePath: dyn.MustPathFromString("resources.jobs.job0.name"), deletePath: dyn.MustPathFromString("resources.jobs.job0.name"), insertPath: dyn.MustPathFromString("resources.jobs.job0.name"), - deleteError: fmt.Errorf("unexpected change at \"resources.jobs.job0.name\" (delete)"), - insertError: fmt.Errorf("unexpected change at \"resources.jobs.job0.name\" (insert)"), - updateError: fmt.Errorf("unexpected change at \"resources.jobs.job0.name\" (update)"), + deleteError: errors.New("unexpected change at \"resources.jobs.job0.name\" (delete)"), + insertError: errors.New("unexpected change at \"resources.jobs.job0.name\" (insert)"), + updateError: errors.New("unexpected change at \"resources.jobs.job0.name\" (update)"), }, { name: "load: can't delete an existing job", phase: PythonMutatorPhaseLoad, deletePath: dyn.MustPathFromString("resources.jobs.job0"), - deleteError: fmt.Errorf("unexpected change at \"resources.jobs.job0\" (delete)"), + deleteError: errors.New("unexpected change at \"resources.jobs.job0\" (delete)"), }, { name: "load: can insert 'resources'", @@ -353,9 +354,9 @@ func TestCreateOverrideVisitor(t *testing.T) { deletePath: dyn.MustPathFromString("include[0]"), insertPath: dyn.MustPathFromString("include[0]"), updatePath: dyn.MustPathFromString("include[0]"), - deleteError: fmt.Errorf("unexpected change at \"include[0]\" (delete)"), - insertError: fmt.Errorf("unexpected change at \"include[0]\" (insert)"), - updateError: fmt.Errorf("unexpected change at \"include[0]\" (update)"), + deleteError: errors.New("unexpected change at \"include[0]\" (delete)"), + insertError: errors.New("unexpected change at \"include[0]\" (insert)"), + updateError: errors.New("unexpected change at \"include[0]\" (update)"), }, { name: "init: can change an existing job", @@ -371,7 +372,7 @@ func TestCreateOverrideVisitor(t *testing.T) { name: "init: can't delete an existing job", phase: PythonMutatorPhaseInit, deletePath: dyn.MustPathFromString("resources.jobs.job0"), - deleteError: fmt.Errorf("unexpected change at \"resources.jobs.job0\" (delete)"), + deleteError: errors.New("unexpected change at \"resources.jobs.job0\" (delete)"), }, { name: "init: can insert 'resources'", @@ -397,9 +398,9 @@ func TestCreateOverrideVisitor(t *testing.T) { deletePath: dyn.MustPathFromString("include[0]"), insertPath: dyn.MustPathFromString("include[0]"), updatePath: dyn.MustPathFromString("include[0]"), - deleteError: fmt.Errorf("unexpected change at \"include[0]\" (delete)"), - insertError: fmt.Errorf("unexpected change at \"include[0]\" (insert)"), - updateError: fmt.Errorf("unexpected change at \"include[0]\" (update)"), + deleteError: errors.New("unexpected change at \"include[0]\" (delete)"), + insertError: errors.New("unexpected change at \"include[0]\" (insert)"), + updateError: errors.New("unexpected change at \"include[0]\" (update)"), }, } diff --git a/bundle/config/mutator/resolve_variable_references.go b/bundle/config/mutator/resolve_variable_references.go index 8c207e375..e074c2b84 100644 --- a/bundle/config/mutator/resolve_variable_references.go +++ b/bundle/config/mutator/resolve_variable_references.go @@ -2,7 +2,7 @@ package mutator import ( "context" - "fmt" + "errors" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config/variable" @@ -68,7 +68,7 @@ func lookupForComplexVariables(v dyn.Value, path dyn.Path) (dyn.Value, error) { } if vv.Type == variable.VariableTypeComplex { - return dyn.InvalidValue, fmt.Errorf("complex variables cannot contain references to another complex variables") + return dyn.InvalidValue, errors.New("complex variables cannot contain references to another complex variables") } return lookup(v, path) @@ -100,7 +100,7 @@ func lookupForVariables(v dyn.Value, path dyn.Path) (dyn.Value, error) { } if vv.Lookup != nil && vv.Lookup.String() != "" { - return dyn.InvalidValue, fmt.Errorf("lookup variables cannot contain references to another lookup variables") + return dyn.InvalidValue, errors.New("lookup variables cannot contain references to another lookup variables") } return lookup(v, path) diff --git a/bundle/config/resources/clusters.go b/bundle/config/resources/clusters.go index ba991e865..073f40a79 100644 --- a/bundle/config/resources/clusters.go +++ b/bundle/config/resources/clusters.go @@ -2,7 +2,6 @@ package resources import ( "context" - "fmt" "net/url" "github.com/databricks/cli/libs/log" @@ -45,7 +44,7 @@ func (s *Cluster) InitializeURL(baseURL url.URL) { if s.ID == "" { return } - baseURL.Path = fmt.Sprintf("compute/clusters/%s", s.ID) + baseURL.Path = "compute/clusters/" + s.ID s.URL = baseURL.String() } diff --git a/bundle/config/resources/job.go b/bundle/config/resources/job.go index 0aa41b2e8..76de78439 100644 --- a/bundle/config/resources/job.go +++ b/bundle/config/resources/job.go @@ -2,7 +2,6 @@ package resources import ( "context" - "fmt" "net/url" "strconv" @@ -52,7 +51,7 @@ func (j *Job) InitializeURL(baseURL url.URL) { if j.ID == "" { return } - baseURL.Path = fmt.Sprintf("jobs/%s", j.ID) + baseURL.Path = "jobs/" + j.ID j.URL = baseURL.String() } diff --git a/bundle/config/resources/mlflow_experiment.go b/bundle/config/resources/mlflow_experiment.go index 5d179ec0f..ea18ce114 100644 --- a/bundle/config/resources/mlflow_experiment.go +++ b/bundle/config/resources/mlflow_experiment.go @@ -2,7 +2,6 @@ package resources import ( "context" - "fmt" "net/url" "github.com/databricks/cli/libs/log" @@ -47,7 +46,7 @@ func (s *MlflowExperiment) InitializeURL(baseURL url.URL) { if s.ID == "" { return } - baseURL.Path = fmt.Sprintf("ml/experiments/%s", s.ID) + baseURL.Path = "ml/experiments/" + s.ID s.URL = baseURL.String() } diff --git a/bundle/config/resources/mlflow_model.go b/bundle/config/resources/mlflow_model.go index 72376f45d..69ae2d438 100644 --- a/bundle/config/resources/mlflow_model.go +++ b/bundle/config/resources/mlflow_model.go @@ -2,7 +2,6 @@ package resources import ( "context" - "fmt" "net/url" "github.com/databricks/cli/libs/log" @@ -47,7 +46,7 @@ func (s *MlflowModel) InitializeURL(baseURL url.URL) { if s.ID == "" { return } - baseURL.Path = fmt.Sprintf("ml/models/%s", s.ID) + baseURL.Path = "ml/models/" + s.ID s.URL = baseURL.String() } diff --git a/bundle/config/resources/model_serving_endpoint.go b/bundle/config/resources/model_serving_endpoint.go index a3c472b3f..8b1394d86 100644 --- a/bundle/config/resources/model_serving_endpoint.go +++ b/bundle/config/resources/model_serving_endpoint.go @@ -2,7 +2,6 @@ package resources import ( "context" - "fmt" "net/url" "github.com/databricks/cli/libs/log" @@ -55,7 +54,7 @@ func (s *ModelServingEndpoint) InitializeURL(baseURL url.URL) { if s.ID == "" { return } - baseURL.Path = fmt.Sprintf("ml/endpoints/%s", s.ID) + baseURL.Path = "ml/endpoints/" + s.ID s.URL = baseURL.String() } diff --git a/bundle/config/resources/permission.go b/bundle/config/resources/permission.go index 62e18a09e..fa1568601 100644 --- a/bundle/config/resources/permission.go +++ b/bundle/config/resources/permission.go @@ -25,5 +25,5 @@ func (p Permission) String() string { return fmt.Sprintf("level: %s, group_name: %s", p.Level, p.GroupName) } - return fmt.Sprintf("level: %s", p.Level) + return "level: " + p.Level } diff --git a/bundle/config/resources/pipeline.go b/bundle/config/resources/pipeline.go index eaa4c5368..5127d07ba 100644 --- a/bundle/config/resources/pipeline.go +++ b/bundle/config/resources/pipeline.go @@ -2,7 +2,6 @@ package resources import ( "context" - "fmt" "net/url" "github.com/databricks/cli/libs/log" @@ -47,7 +46,7 @@ func (p *Pipeline) InitializeURL(baseURL url.URL) { if p.ID == "" { return } - baseURL.Path = fmt.Sprintf("pipelines/%s", p.ID) + baseURL.Path = "pipelines/" + p.ID p.URL = baseURL.String() } diff --git a/bundle/config/resources/quality_monitor.go b/bundle/config/resources/quality_monitor.go index b1d7e08a5..88bc0a3e7 100644 --- a/bundle/config/resources/quality_monitor.go +++ b/bundle/config/resources/quality_monitor.go @@ -2,7 +2,6 @@ package resources import ( "context" - "fmt" "net/url" "strings" @@ -51,7 +50,7 @@ func (s *QualityMonitor) InitializeURL(baseURL url.URL) { if s.TableName == "" { return } - baseURL.Path = fmt.Sprintf("explore/data/%s", strings.ReplaceAll(s.TableName, ".", "/")) + baseURL.Path = "explore/data/" + strings.ReplaceAll(s.TableName, ".", "/") s.URL = baseURL.String() } diff --git a/bundle/config/resources/registered_model.go b/bundle/config/resources/registered_model.go index 8513a79ae..006eef773 100644 --- a/bundle/config/resources/registered_model.go +++ b/bundle/config/resources/registered_model.go @@ -2,7 +2,6 @@ package resources import ( "context" - "fmt" "net/url" "strings" @@ -57,7 +56,7 @@ func (s *RegisteredModel) InitializeURL(baseURL url.URL) { if s.ID == "" { return } - baseURL.Path = fmt.Sprintf("explore/data/models/%s", strings.ReplaceAll(s.ID, ".", "/")) + baseURL.Path = "explore/data/models/" + strings.ReplaceAll(s.ID, ".", "/") s.URL = baseURL.String() } diff --git a/bundle/config/resources/schema.go b/bundle/config/resources/schema.go index 8eadd7e46..b638907ac 100644 --- a/bundle/config/resources/schema.go +++ b/bundle/config/resources/schema.go @@ -2,7 +2,7 @@ package resources import ( "context" - "fmt" + "errors" "net/url" "strings" @@ -26,7 +26,7 @@ type Schema struct { } func (s *Schema) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) { - return false, fmt.Errorf("schema.Exists() is not supported") + return false, errors.New("schema.Exists() is not supported") } func (s *Schema) TerraformResourceName() string { @@ -37,7 +37,7 @@ func (s *Schema) InitializeURL(baseURL url.URL) { if s.ID == "" { return } - baseURL.Path = fmt.Sprintf("explore/data/%s", strings.ReplaceAll(s.ID, ".", "/")) + baseURL.Path = "explore/data/" + strings.ReplaceAll(s.ID, ".", "/") s.URL = baseURL.String() } diff --git a/bundle/config/resources/volume.go b/bundle/config/resources/volume.go index cae2a3463..882b7107d 100644 --- a/bundle/config/resources/volume.go +++ b/bundle/config/resources/volume.go @@ -2,7 +2,7 @@ package resources import ( "context" - "fmt" + "errors" "net/url" "strings" @@ -34,7 +34,7 @@ func (v Volume) MarshalJSON() ([]byte, error) { } func (v *Volume) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) { - return false, fmt.Errorf("volume.Exists() is not supported") + return false, errors.New("volume.Exists() is not supported") } func (v *Volume) TerraformResourceName() string { @@ -45,7 +45,7 @@ func (v *Volume) InitializeURL(baseURL url.URL) { if v.ID == "" { return } - baseURL.Path = fmt.Sprintf("explore/data/volumes/%s", strings.ReplaceAll(v.ID, ".", "/")) + baseURL.Path = "explore/data/volumes/" + strings.ReplaceAll(v.ID, ".", "/") v.URL = baseURL.String() } diff --git a/bundle/config/validate/folder_permissions.go b/bundle/config/validate/folder_permissions.go index 5b28d599e..7b12b4b16 100644 --- a/bundle/config/validate/folder_permissions.go +++ b/bundle/config/validate/folder_permissions.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path" + "strconv" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/libraries" @@ -60,7 +61,7 @@ func checkFolderPermission(ctx context.Context, b bundle.ReadOnlyBundle, folderP } objPermissions, err := w.GetPermissions(ctx, workspace.GetWorkspaceObjectPermissionsRequest{ - WorkspaceObjectId: fmt.Sprint(obj.ObjectId), + WorkspaceObjectId: strconv.FormatInt(obj.ObjectId, 10), WorkspaceObjectType: "directories", }) if err != nil { diff --git a/bundle/config/validate/unique_resource_keys.go b/bundle/config/validate/unique_resource_keys.go index 50295375b..d80c5d632 100644 --- a/bundle/config/validate/unique_resource_keys.go +++ b/bundle/config/validate/unique_resource_keys.go @@ -2,7 +2,6 @@ package validate import ( "context" - "fmt" "sort" "github.com/databricks/cli/bundle" @@ -102,7 +101,7 @@ func (m *uniqueResourceKeys) Apply(ctx context.Context, b *bundle.Bundle) diag.D // If there are multiple resources with the same key, report an error. diags = append(diags, diag.Diagnostic{ Severity: diag.Error, - Summary: fmt.Sprintf("multiple resources have been defined with the same key: %s", k), + Summary: "multiple resources have been defined with the same key: " + k, Locations: v.locations, Paths: v.paths, }) diff --git a/bundle/config/validate/validate_artifact_path.go b/bundle/config/validate/validate_artifact_path.go index 5bab99ccf..aa4492670 100644 --- a/bundle/config/validate/validate_artifact_path.go +++ b/bundle/config/validate/validate_artifact_path.go @@ -68,7 +68,7 @@ func findVolumeInBundle(r config.Root, catalogName, schemaName, volumeName strin if v.SchemaName != schemaName && !isSchemaDefinedInBundle { continue } - pathString := fmt.Sprintf("resources.volumes.%s", k) + pathString := "resources.volumes." + k return dyn.MustPathFromString(pathString), r.GetLocations(pathString), true } return nil, nil, false diff --git a/bundle/config/validate/validate_artifact_path_test.go b/bundle/config/validate/validate_artifact_path_test.go index 8fb5c9618..e1ae6af34 100644 --- a/bundle/config/validate/validate_artifact_path_test.go +++ b/bundle/config/validate/validate_artifact_path_test.go @@ -2,7 +2,6 @@ package validate import ( "context" - "fmt" "testing" "github.com/databricks/cli/bundle" @@ -152,7 +151,7 @@ func TestExtractVolumeFromPath(t *testing.T) { for _, p := range invalidVolumePaths() { _, _, _, err := extractVolumeFromPath(p) - assert.EqualError(t, err, fmt.Sprintf("expected UC volume path to be in the format /Volumes////..., got %s", p)) + assert.EqualError(t, err, "expected UC volume path to be in the format /Volumes////..., got "+p) } } @@ -171,7 +170,7 @@ func TestValidateArtifactPathWithInvalidPaths(t *testing.T) { diags := bundle.ApplyReadOnly(context.Background(), bundle.ReadOnly(b), ValidateArtifactPath()) require.Equal(t, diag.Diagnostics{{ Severity: diag.Error, - Summary: fmt.Sprintf("expected UC volume path to be in the format /Volumes////..., got %s", p), + Summary: "expected UC volume path to be in the format /Volumes////..., got " + p, Locations: []dyn.Location{{File: "config.yml", Line: 1, Column: 2}}, Paths: []dyn.Path{dyn.MustPathFromString("workspace.artifact_path")}, }}, diags) diff --git a/bundle/config/variable/lookup.go b/bundle/config/variable/lookup.go index 37e380f18..71c8512e3 100755 --- a/bundle/config/variable/lookup.go +++ b/bundle/config/variable/lookup.go @@ -2,7 +2,7 @@ package variable import ( "context" - "fmt" + "errors" "github.com/databricks/databricks-sdk-go" ) @@ -83,11 +83,11 @@ func (l *Lookup) constructResolver() (resolver, error) { switch len(resolvers) { case 0: - return nil, fmt.Errorf("no valid lookup fields provided") + return nil, errors.New("no valid lookup fields provided") case 1: return resolvers[0], nil default: - return nil, fmt.Errorf("exactly one lookup field must be provided") + return nil, errors.New("exactly one lookup field must be provided") } } diff --git a/bundle/config/variable/resolve_alert.go b/bundle/config/variable/resolve_alert.go index be83e81fa..507306aa0 100644 --- a/bundle/config/variable/resolve_alert.go +++ b/bundle/config/variable/resolve_alert.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolveAlert) Resolve(ctx context.Context, w *databricks.WorkspaceClient if err != nil { return "", err } - return fmt.Sprint(entity.Id), nil + return entity.Id, nil } func (l resolveAlert) String() string { - return fmt.Sprintf("alert: %s", l.name) + return "alert: " + l.name } diff --git a/bundle/config/variable/resolve_cluster.go b/bundle/config/variable/resolve_cluster.go index a8cf3fe7f..51278aef5 100644 --- a/bundle/config/variable/resolve_cluster.go +++ b/bundle/config/variable/resolve_cluster.go @@ -42,5 +42,5 @@ func (l resolveCluster) Resolve(ctx context.Context, w *databricks.WorkspaceClie } func (l resolveCluster) String() string { - return fmt.Sprintf("cluster: %s", l.name) + return "cluster: " + l.name } diff --git a/bundle/config/variable/resolve_cluster_policy.go b/bundle/config/variable/resolve_cluster_policy.go index b19380a63..94fd892b2 100644 --- a/bundle/config/variable/resolve_cluster_policy.go +++ b/bundle/config/variable/resolve_cluster_policy.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolveClusterPolicy) Resolve(ctx context.Context, w *databricks.Workspa if err != nil { return "", err } - return fmt.Sprint(entity.PolicyId), nil + return entity.PolicyId, nil } func (l resolveClusterPolicy) String() string { - return fmt.Sprintf("cluster-policy: %s", l.name) + return "cluster-policy: " + l.name } diff --git a/bundle/config/variable/resolve_dashboard.go b/bundle/config/variable/resolve_dashboard.go index 44fd45197..2979716ce 100644 --- a/bundle/config/variable/resolve_dashboard.go +++ b/bundle/config/variable/resolve_dashboard.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolveDashboard) Resolve(ctx context.Context, w *databricks.WorkspaceCl if err != nil { return "", err } - return fmt.Sprint(entity.Id), nil + return entity.Id, nil } func (l resolveDashboard) String() string { - return fmt.Sprintf("dashboard: %s", l.name) + return "dashboard: " + l.name } diff --git a/bundle/config/variable/resolve_instance_pool.go b/bundle/config/variable/resolve_instance_pool.go index cbf0775c9..600b47a50 100644 --- a/bundle/config/variable/resolve_instance_pool.go +++ b/bundle/config/variable/resolve_instance_pool.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolveInstancePool) Resolve(ctx context.Context, w *databricks.Workspac if err != nil { return "", err } - return fmt.Sprint(entity.InstancePoolId), nil + return entity.InstancePoolId, nil } func (l resolveInstancePool) String() string { - return fmt.Sprintf("instance-pool: %s", l.name) + return "instance-pool: " + l.name } diff --git a/bundle/config/variable/resolve_job.go b/bundle/config/variable/resolve_job.go index 3def64888..4fe6ae3e7 100644 --- a/bundle/config/variable/resolve_job.go +++ b/bundle/config/variable/resolve_job.go @@ -2,7 +2,7 @@ package variable import ( "context" - "fmt" + "strconv" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +16,9 @@ func (l resolveJob) Resolve(ctx context.Context, w *databricks.WorkspaceClient) if err != nil { return "", err } - return fmt.Sprint(entity.JobId), nil + return strconv.FormatInt(entity.JobId, 10), nil } func (l resolveJob) String() string { - return fmt.Sprintf("job: %s", l.name) + return "job: " + l.name } diff --git a/bundle/config/variable/resolve_metastore.go b/bundle/config/variable/resolve_metastore.go index 958e43787..8a0a8c7ed 100644 --- a/bundle/config/variable/resolve_metastore.go +++ b/bundle/config/variable/resolve_metastore.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolveMetastore) Resolve(ctx context.Context, w *databricks.WorkspaceCl if err != nil { return "", err } - return fmt.Sprint(entity.MetastoreId), nil + return entity.MetastoreId, nil } func (l resolveMetastore) String() string { - return fmt.Sprintf("metastore: %s", l.name) + return "metastore: " + l.name } diff --git a/bundle/config/variable/resolve_notification_destination.go b/bundle/config/variable/resolve_notification_destination.go index 4c4cd892a..4696a52c8 100644 --- a/bundle/config/variable/resolve_notification_destination.go +++ b/bundle/config/variable/resolve_notification_destination.go @@ -42,5 +42,5 @@ func (l resolveNotificationDestination) Resolve(ctx context.Context, w *databric } func (l resolveNotificationDestination) String() string { - return fmt.Sprintf("notification-destination: %s", l.name) + return "notification-destination: " + l.name } diff --git a/bundle/config/variable/resolve_notification_destination_test.go b/bundle/config/variable/resolve_notification_destination_test.go index 2b8201d15..f44b2f3e9 100644 --- a/bundle/config/variable/resolve_notification_destination_test.go +++ b/bundle/config/variable/resolve_notification_destination_test.go @@ -2,7 +2,7 @@ package variable import ( "context" - "fmt" + "errors" "testing" "github.com/databricks/databricks-sdk-go/experimental/mocks" @@ -35,7 +35,7 @@ func TestResolveNotificationDestination_ResolveError(t *testing.T) { api := m.GetMockNotificationDestinationsAPI() api.EXPECT(). ListAll(mock.Anything, mock.Anything). - Return(nil, fmt.Errorf("bad")) + Return(nil, errors.New("bad")) ctx := context.Background() l := resolveNotificationDestination{name: "destination"} diff --git a/bundle/config/variable/resolve_pipeline.go b/bundle/config/variable/resolve_pipeline.go index cabc620da..33b14530d 100644 --- a/bundle/config/variable/resolve_pipeline.go +++ b/bundle/config/variable/resolve_pipeline.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolvePipeline) Resolve(ctx context.Context, w *databricks.WorkspaceCli if err != nil { return "", err } - return fmt.Sprint(entity.PipelineId), nil + return entity.PipelineId, nil } func (l resolvePipeline) String() string { - return fmt.Sprintf("pipeline: %s", l.name) + return "pipeline: " + l.name } diff --git a/bundle/config/variable/resolve_query.go b/bundle/config/variable/resolve_query.go index 602ff8deb..88f653dc6 100644 --- a/bundle/config/variable/resolve_query.go +++ b/bundle/config/variable/resolve_query.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolveQuery) Resolve(ctx context.Context, w *databricks.WorkspaceClient if err != nil { return "", err } - return fmt.Sprint(entity.Id), nil + return entity.Id, nil } func (l resolveQuery) String() string { - return fmt.Sprintf("query: %s", l.name) + return "query: " + l.name } diff --git a/bundle/config/variable/resolve_service_principal.go b/bundle/config/variable/resolve_service_principal.go index 3bea4314b..03b8e3089 100644 --- a/bundle/config/variable/resolve_service_principal.go +++ b/bundle/config/variable/resolve_service_principal.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolveServicePrincipal) Resolve(ctx context.Context, w *databricks.Work if err != nil { return "", err } - return fmt.Sprint(entity.ApplicationId), nil + return entity.ApplicationId, nil } func (l resolveServicePrincipal) String() string { - return fmt.Sprintf("service-principal: %s", l.name) + return "service-principal: " + l.name } diff --git a/bundle/config/variable/resolve_warehouse.go b/bundle/config/variable/resolve_warehouse.go index fbd3663a2..cabdb1160 100644 --- a/bundle/config/variable/resolve_warehouse.go +++ b/bundle/config/variable/resolve_warehouse.go @@ -2,7 +2,6 @@ package variable import ( "context" - "fmt" "github.com/databricks/databricks-sdk-go" ) @@ -16,9 +15,9 @@ func (l resolveWarehouse) Resolve(ctx context.Context, w *databricks.WorkspaceCl if err != nil { return "", err } - return fmt.Sprint(entity.Id), nil + return entity.Id, nil } func (l resolveWarehouse) String() string { - return fmt.Sprintf("warehouse: %s", l.name) + return "warehouse: " + l.name } diff --git a/bundle/config/variable/variable.go b/bundle/config/variable/variable.go index 2362ad10d..95a68cfeb 100644 --- a/bundle/config/variable/variable.go +++ b/bundle/config/variable/variable.go @@ -1,6 +1,7 @@ package variable import ( + "errors" "fmt" "reflect" ) @@ -68,7 +69,7 @@ func (v *Variable) Set(val VariableValue) error { switch rv.Kind() { case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map: if v.Type != VariableTypeComplex { - return fmt.Errorf("variable type is not complex") + return errors.New("variable type is not complex") } } diff --git a/bundle/deploy/state.go b/bundle/deploy/state.go index a131ab9c3..6e285034a 100644 --- a/bundle/deploy/state.go +++ b/bundle/deploy/state.go @@ -3,6 +3,7 @@ package deploy import ( "context" "encoding/json" + "errors" "fmt" "io" "io/fs" @@ -95,7 +96,7 @@ func (e *entry) Type() fs.FileMode { func (e *entry) Info() (fs.FileInfo, error) { if e.info == nil { - return nil, fmt.Errorf("no info available") + return nil, errors.New("no info available") } return e.info, nil } diff --git a/bundle/deploy/terraform/check_dashboards_modified_remotely.go b/bundle/deploy/terraform/check_dashboards_modified_remotely.go index f263e8a7f..66914af54 100644 --- a/bundle/deploy/terraform/check_dashboards_modified_remotely.go +++ b/bundle/deploy/terraform/check_dashboards_modified_remotely.go @@ -72,7 +72,7 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B continue } - path := dyn.MustPathFromString(fmt.Sprintf("resources.dashboards.%s", dashboard.Name)) + path := dyn.MustPathFromString("resources.dashboards." + dashboard.Name) loc := b.Config.GetLocation(path.String()) actual, err := b.WorkspaceClient().Lakeview.GetByDashboardId(ctx, dashboard.ID) if err != nil { diff --git a/bundle/deploy/terraform/check_dashboards_modified_remotely_test.go b/bundle/deploy/terraform/check_dashboards_modified_remotely_test.go index 1bed3b1be..46bdc1f38 100644 --- a/bundle/deploy/terraform/check_dashboards_modified_remotely_test.go +++ b/bundle/deploy/terraform/check_dashboards_modified_remotely_test.go @@ -2,7 +2,7 @@ package terraform import ( "context" - "fmt" + "errors" "path/filepath" "testing" @@ -122,7 +122,7 @@ func TestCheckDashboardsModifiedRemotely_ExistingStateFailureToGet(t *testing.T) dashboardsAPI := m.GetMockLakeviewAPI() dashboardsAPI.EXPECT(). GetByDashboardId(mock.Anything, "id1"). - Return(nil, fmt.Errorf("failure")). + Return(nil, errors.New("failure")). Once() b.SetWorkpaceClient(m.WorkspaceClient) diff --git a/bundle/deploy/terraform/init.go b/bundle/deploy/terraform/init.go index 366f0be8c..e69f0bf0f 100644 --- a/bundle/deploy/terraform/init.go +++ b/bundle/deploy/terraform/init.go @@ -230,7 +230,7 @@ func setUserAgentExtraEnvVar(environ map[string]string, b *bundle.Bundle) error // Add "cli" to the user agent in set by the Databricks Terraform provider. // This will allow us to attribute downstream requests made by the Databricks // Terraform provider to the CLI. - products := []string{fmt.Sprintf("cli/%s", build.GetInfo().Version)} + products := []string{"cli/" + build.GetInfo().Version} if experimental := b.Config.Experimental; experimental != nil { if experimental.PyDABs.Enabled { products = append(products, "databricks-pydabs/0.0.0") diff --git a/bundle/deploy/terraform/load.go b/bundle/deploy/terraform/load.go index 3fb76855e..1c563fa77 100644 --- a/bundle/deploy/terraform/load.go +++ b/bundle/deploy/terraform/load.go @@ -2,6 +2,7 @@ package terraform import ( "context" + "errors" "fmt" "slices" @@ -58,7 +59,7 @@ func (l *load) validateState(state *resourcesState) error { } if len(state.Resources) == 0 && slices.Contains(l.modes, ErrorOnEmptyState) { - return fmt.Errorf("no deployment state. Did you forget to run 'databricks bundle deploy'?") + return errors.New("no deployment state. Did you forget to run 'databricks bundle deploy'?") } return nil diff --git a/bundle/libraries/helpers.go b/bundle/libraries/helpers.go index 2149e5885..5a1a9511c 100644 --- a/bundle/libraries/helpers.go +++ b/bundle/libraries/helpers.go @@ -1,7 +1,7 @@ package libraries import ( - "fmt" + "errors" "github.com/databricks/databricks-sdk-go/service/compute" ) @@ -20,5 +20,5 @@ func libraryPath(library *compute.Library) (string, error) { return library.Requirements, nil } - return "", fmt.Errorf("not supported library type") + return "", errors.New("not supported library type") } diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index 4ac0d38a5..828b12f50 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -3,6 +3,7 @@ package permissions import ( "context" "fmt" + "strconv" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/libraries" @@ -78,7 +79,7 @@ func setPermissions(ctx context.Context, w workspace.WorkspaceInterface, path st } _, err = w.SetPermissions(ctx, workspace.WorkspaceObjectPermissionsRequest{ - WorkspaceObjectId: fmt.Sprint(obj.ObjectId), + WorkspaceObjectId: strconv.FormatInt(obj.ObjectId, 10), WorkspaceObjectType: "directories", AccessControlList: permissions, }) diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 2dc9623bd..16595611f 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -2,7 +2,7 @@ package phases import ( "context" - "fmt" + "errors" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/artifacts" @@ -54,7 +54,7 @@ func filterDeleteOrRecreateActions(changes []*tfjson.ResourceChange, resourceTyp func approvalForDeploy(ctx context.Context, b *bundle.Bundle) (bool, error) { tf := b.Terraform if tf == nil { - return false, fmt.Errorf("terraform not initialized") + return false, errors.New("terraform not initialized") } // read plan file @@ -111,7 +111,7 @@ is removed from the catalog, but the underlying files are not deleted:` } if !cmdio.IsPromptSupported(ctx) { - return false, fmt.Errorf("the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed") + return false, errors.New("the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed") } cmdio.LogString(ctx, "") diff --git a/bundle/phases/destroy.go b/bundle/phases/destroy.go index 6eb8b6a01..05a41dea2 100644 --- a/bundle/phases/destroy.go +++ b/bundle/phases/destroy.go @@ -3,7 +3,6 @@ package phases import ( "context" "errors" - "fmt" "net/http" "github.com/databricks/cli/bundle" @@ -34,7 +33,7 @@ func assertRootPathExists(ctx context.Context, b *bundle.Bundle) (bool, error) { func approvalForDestroy(ctx context.Context, b *bundle.Bundle) (bool, error) { tf := b.Terraform if tf == nil { - return false, fmt.Errorf("terraform not initialized") + return false, errors.New("terraform not initialized") } // read plan file @@ -63,7 +62,7 @@ func approvalForDestroy(ctx context.Context, b *bundle.Bundle) (bool, error) { } - cmdio.LogString(ctx, fmt.Sprintf("All files and directories at the following location will be deleted: %s", b.Config.Workspace.RootPath)) + cmdio.LogString(ctx, "All files and directories at the following location will be deleted: "+b.Config.Workspace.RootPath) cmdio.LogString(ctx, "") if b.AutoApprove { diff --git a/bundle/root.go b/bundle/root.go index efc21e0ca..9ea9a8c13 100644 --- a/bundle/root.go +++ b/bundle/root.go @@ -2,6 +2,7 @@ package bundle import ( "context" + "errors" "fmt" "os" @@ -21,7 +22,7 @@ func getRootEnv(ctx context.Context) (string, error) { } stat, err := os.Stat(path) if err == nil && !stat.IsDir() { - err = fmt.Errorf("not a directory") + err = errors.New("not a directory") } if err != nil { return "", fmt.Errorf(`invalid bundle root %s="%s": %w`, env.RootVariable, path, err) diff --git a/bundle/run/job.go b/bundle/run/job.go index b43db9184..2489ca619 100644 --- a/bundle/run/job.go +++ b/bundle/run/job.go @@ -3,6 +3,7 @@ package run import ( "context" "encoding/json" + "errors" "fmt" "strconv" "time" @@ -181,13 +182,13 @@ func (r *jobRunner) Run(ctx context.Context, opts *Options) (output.RunOutput, e // callback to log progress events. Called on every poll request progressLogger, ok := cmdio.FromContext(ctx) if !ok { - return nil, fmt.Errorf("no progress logger found") + return nil, errors.New("no progress logger found") } logProgress := logProgressCallback(ctx, progressLogger) waiter, err := w.Jobs.RunNow(ctx, *req) if err != nil { - return nil, fmt.Errorf("cannot start job") + return nil, errors.New("cannot start job") } if opts.NoWait { @@ -266,7 +267,7 @@ func (r *jobRunner) convertPythonParams(opts *Options) error { if len(opts.Job.pythonParams) > 0 { if _, ok := opts.Job.notebookParams["__python_params"]; ok { - return fmt.Errorf("can't use __python_params as notebook param, the name is reserved for internal use") + return errors.New("can't use __python_params as notebook param, the name is reserved for internal use") } p, err := json.Marshal(opts.Job.pythonParams) if err != nil { diff --git a/bundle/run/job_options.go b/bundle/run/job_options.go index 6a03dff95..7db8e72cd 100644 --- a/bundle/run/job_options.go +++ b/bundle/run/job_options.go @@ -1,7 +1,7 @@ package run import ( - "fmt" + "errors" "strconv" "github.com/databricks/cli/bundle/config/resources" @@ -60,16 +60,16 @@ func (o *JobOptions) hasJobParametersConfigured() bool { // Validate returns if the combination of options is valid. func (o *JobOptions) Validate(job *resources.Job) error { if job == nil { - return fmt.Errorf("job not defined") + return errors.New("job not defined") } // Ensure mutual exclusion on job parameters and task parameters. hasJobParams := len(job.Parameters) > 0 if hasJobParams && o.hasTaskParametersConfigured() { - return fmt.Errorf("the job to run defines job parameters; specifying task parameters is not allowed") + return errors.New("the job to run defines job parameters; specifying task parameters is not allowed") } if !hasJobParams && o.hasJobParametersConfigured() { - return fmt.Errorf("the job to run does not define job parameters; specifying job parameters is not allowed") + return errors.New("the job to run does not define job parameters; specifying job parameters is not allowed") } return nil @@ -80,7 +80,7 @@ func (o *JobOptions) validatePipelineParams() (*jobs.PipelineParams, error) { return nil, nil } - defaultErr := fmt.Errorf("job run argument --pipeline-params only supports `full_refresh=`") + defaultErr := errors.New("job run argument --pipeline-params only supports `full_refresh=`") v, ok := o.pipelineParams["full_refresh"] if !ok { return nil, defaultErr diff --git a/bundle/run/output/job.go b/bundle/run/output/job.go index 6199ac2f7..2ac974cd5 100644 --- a/bundle/run/output/job.go +++ b/bundle/run/output/job.go @@ -47,7 +47,7 @@ func (out *JobOutput) String() (string, error) { } result.WriteString("=======\n") result.WriteString(fmt.Sprintf("Task %s:\n", v.TaskKey)) - result.WriteString(fmt.Sprintf("%s\n", taskString)) + result.WriteString(taskString + "\n") } return result.String(), nil } diff --git a/bundle/run/output/task.go b/bundle/run/output/task.go index 1ef78a8c3..53b989e88 100644 --- a/bundle/run/output/task.go +++ b/bundle/run/output/task.go @@ -2,7 +2,6 @@ package output import ( "encoding/json" - "fmt" "github.com/databricks/databricks-sdk-go/service/jobs" ) @@ -27,7 +26,7 @@ func structToString(val any) (string, error) { func (out *NotebookOutput) String() (string, error) { if out.Truncated { - return fmt.Sprintf("%s\n[truncated...]\n", out.Result), nil + return out.Result + "\n[truncated...]\n", nil } return out.Result, nil } @@ -42,7 +41,7 @@ func (out *DbtOutput) String() (string, error) { // JSON is used because it's a convenient representation. // If user needs machine parsable output, they can use the --output json // flag - return fmt.Sprintf("Dbt Task Output:\n%s", outputString), nil + return "Dbt Task Output:\n" + outputString, nil } func (out *SqlOutput) String() (string, error) { @@ -55,12 +54,12 @@ func (out *SqlOutput) String() (string, error) { // JSON is used because it's a convenient representation. // If user needs machine parsable output, they can use the --output json // flag - return fmt.Sprintf("SQL Task Output:\n%s", outputString), nil + return "SQL Task Output:\n" + outputString, nil } func (out *LogsOutput) String() (string, error) { if out.LogsTruncated { - return fmt.Sprintf("%s\n[truncated...]\n", out.Logs), nil + return out.Logs + "\n[truncated...]\n", nil } return out.Logs, nil } diff --git a/bundle/run/pipeline.go b/bundle/run/pipeline.go index c447f044a..bdcf0f142 100644 --- a/bundle/run/pipeline.go +++ b/bundle/run/pipeline.go @@ -2,6 +2,7 @@ package run import ( "context" + "errors" "fmt" "time" @@ -33,7 +34,7 @@ func (r *pipelineRunner) logEvent(ctx context.Context, event pipelines.PipelineE if event.Error != nil && len(event.Error.Exceptions) > 0 { logString += "trace for most recent exception: \n" for i := range len(event.Error.Exceptions) { - logString += fmt.Sprintf("%s\n", event.Error.Exceptions[i].Message) + logString += event.Error.Exceptions[i].Message + "\n" } } if logString != "" { @@ -107,7 +108,7 @@ func (r *pipelineRunner) Run(ctx context.Context, opts *Options) (output.RunOutp updateTracker := progress.NewUpdateTracker(pipelineID, updateID, w) progressLogger, ok := cmdio.FromContext(ctx) if !ok { - return nil, fmt.Errorf("no progress logger found") + return nil, errors.New("no progress logger found") } // Log the pipeline update URL as soon as it is available. @@ -144,7 +145,7 @@ func (r *pipelineRunner) Run(ctx context.Context, opts *Options) (output.RunOutp if state == pipelines.UpdateInfoStateCanceled { log.Infof(ctx, "Update was cancelled!") - return nil, fmt.Errorf("update cancelled") + return nil, errors.New("update cancelled") } if state == pipelines.UpdateInfoStateFailed { log.Infof(ctx, "Update has failed!") @@ -152,7 +153,7 @@ func (r *pipelineRunner) Run(ctx context.Context, opts *Options) (output.RunOutp if err != nil { return nil, err } - return nil, fmt.Errorf("update failed") + return nil, errors.New("update failed") } if state == pipelines.UpdateInfoStateCompleted { log.Infof(ctx, "Update has completed successfully!") diff --git a/bundle/run/progress/pipeline.go b/bundle/run/progress/pipeline.go index b82dd7abd..ce92c4cde 100644 --- a/bundle/run/progress/pipeline.go +++ b/bundle/run/progress/pipeline.go @@ -33,7 +33,7 @@ func (event *ProgressEvent) String() string { // construct error string if level=`Error` if event.Level == pipelines.EventLevelError && event.Error != nil { for _, exception := range event.Error.Exceptions { - result.WriteString(fmt.Sprintf("\n%s", exception.Message)) + result.WriteString("\n" + exception.Message) } } return result.String() diff --git a/bundle/tests/run_as_test.go b/bundle/tests/run_as_test.go index 113a6140b..03ff51ec5 100644 --- a/bundle/tests/run_as_test.go +++ b/bundle/tests/run_as_test.go @@ -2,7 +2,6 @@ package config_tests import ( "context" - "fmt" "testing" "github.com/databricks/cli/bundle" @@ -219,7 +218,7 @@ func TestRunAsErrorNeitherUserOrSpSpecified(t *testing.T) { for _, tc := range tcases { t.Run(tc.name, func(t *testing.T) { - bundlePath := fmt.Sprintf("./run_as/not_allowed/neither_sp_nor_user/%s", tc.name) + bundlePath := "./run_as/not_allowed/neither_sp_nor_user/" + tc.name b := load(t, bundlePath) ctx := context.Background() diff --git a/bundle/trampoline/python_wheel.go b/bundle/trampoline/python_wheel.go index 8e309a625..075804479 100644 --- a/bundle/trampoline/python_wheel.go +++ b/bundle/trampoline/python_wheel.go @@ -2,6 +2,7 @@ package trampoline import ( "context" + "errors" "fmt" "strconv" "strings" @@ -147,7 +148,7 @@ func (t *pythonTrampoline) GetTemplateData(task *jobs.Task) (map[string]any, err func (t *pythonTrampoline) generateParameters(task *jobs.PythonWheelTask) (string, error) { if task.Parameters != nil && task.NamedParameters != nil { - return "", fmt.Errorf("not allowed to pass both paramaters and named_parameters") + return "", errors.New("not allowed to pass both paramaters and named_parameters") } params := append([]string{task.PackageName}, task.Parameters...) for k, v := range task.NamedParameters { diff --git a/bundle/trampoline/trampoline_test.go b/bundle/trampoline/trampoline_test.go index 3c5d18570..6e6b8db48 100644 --- a/bundle/trampoline/trampoline_test.go +++ b/bundle/trampoline/trampoline_test.go @@ -2,7 +2,7 @@ package trampoline import ( "context" - "fmt" + "errors" "os" "path/filepath" "testing" @@ -30,7 +30,7 @@ func (f *functions) GetTasks(b *bundle.Bundle) []TaskWithJobKey { func (f *functions) GetTemplateData(task *jobs.Task) (map[string]any, error) { if task.PythonWheelTask == nil { - return nil, fmt.Errorf("PythonWheelTask cannot be nil") + return nil, errors.New("PythonWheelTask cannot be nil") } data := make(map[string]any) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index ceceae25c..4261e93e7 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -2,7 +2,7 @@ package auth import ( "context" - "fmt" + "errors" "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cmdio" @@ -36,7 +36,7 @@ GCP: https://docs.gcp.databricks.com/dev-tools/auth/index.html`, func promptForHost(ctx context.Context) (string, error) { if !cmdio.IsInTTY(ctx) { - return "", fmt.Errorf("the command is being run in a non-interactive environment, please specify a host using --host") + return "", errors.New("the command is being run in a non-interactive environment, please specify a host using --host") } prompt := cmdio.Prompt(ctx) @@ -46,7 +46,7 @@ func promptForHost(ctx context.Context) (string, error) { func promptForAccountID(ctx context.Context) (string, error) { if !cmdio.IsInTTY(ctx) { - return "", fmt.Errorf("the command is being run in a non-interactive environment, please specify an account ID using --account-id") + return "", errors.New("the command is being run in a non-interactive environment, please specify an account ID using --account-id") } prompt := cmdio.Prompt(ctx) diff --git a/cmd/auth/describe_test.go b/cmd/auth/describe_test.go index 7f5f900d4..35e0c6e64 100644 --- a/cmd/auth/describe_test.go +++ b/cmd/auth/describe_test.go @@ -2,7 +2,7 @@ package auth import ( "context" - "fmt" + "errors" "testing" "github.com/databricks/cli/cmd/root" @@ -102,7 +102,7 @@ func TestGetWorkspaceAuthStatusError(t *testing.T) { "token": "test-token", "auth_type": "azure-cli", }) - return cfg, false, fmt.Errorf("auth error") + return cfg, false, errors.New("auth error") }) require.NoError(t, err) require.NotNil(t, status) @@ -151,7 +151,7 @@ func TestGetWorkspaceAuthStatusSensitive(t *testing.T) { "token": "test-token", "auth_type": "azure-cli", }) - return cfg, false, fmt.Errorf("auth error") + return cfg, false, errors.New("auth error") }) require.NoError(t, err) require.NotNil(t, status) diff --git a/cmd/auth/env.go b/cmd/auth/env.go index 52b7cbbfd..11149af8c 100644 --- a/cmd/auth/env.go +++ b/cmd/auth/env.go @@ -23,9 +23,9 @@ func canonicalHost(host string) (string, error) { } // If the host is empty, assume the scheme wasn't included. if parsedHost.Host == "" { - return fmt.Sprintf("https://%s", host), nil + return "https://" + host, nil } - return fmt.Sprintf("https://%s", parsedHost.Host), nil + return "https://" + parsedHost.Host, nil } var ErrNoMatchingProfiles = errors.New("no matching profiles found") diff --git a/cmd/auth/login.go b/cmd/auth/login.go index c98676599..a6d0bf4cc 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -176,7 +176,7 @@ depends on the existing profiles you have set in your configuration file func setHostAndAccountId(ctx context.Context, profileName string, persistentAuth *auth.PersistentAuth, args []string) error { // If both [HOST] and --host are provided, return an error. if len(args) > 0 && persistentAuth.Host != "" { - return fmt.Errorf("please only provide a host as an argument or a flag, not both") + return errors.New("please only provide a host as an argument or a flag, not both") } profiler := profile.GetProfiler(ctx) diff --git a/cmd/bundle/destroy.go b/cmd/bundle/destroy.go index 711abbcd7..0b2f14875 100644 --- a/cmd/bundle/destroy.go +++ b/cmd/bundle/destroy.go @@ -2,7 +2,7 @@ package bundle import ( "context" - "fmt" + "errors" "os" "github.com/databricks/cli/bundle" @@ -49,16 +49,16 @@ func newDestroyCommand() *cobra.Command { // we require auto-approve for non tty terminals since interactive consent // is not possible if !term.IsTerminal(int(os.Stderr.Fd())) && !autoApprove { - return fmt.Errorf("please specify --auto-approve to skip interactive confirmation checks for non tty consoles") + return errors.New("please specify --auto-approve to skip interactive confirmation checks for non tty consoles") } // Check auto-approve is selected for json logging logger, ok := cmdio.FromContext(ctx) if !ok { - return fmt.Errorf("progress logger not found") + return errors.New("progress logger not found") } if logger.Mode == flags.ModeJson && !autoApprove { - return fmt.Errorf("please specify --auto-approve since selected logging format is json") + return errors.New("please specify --auto-approve since selected logging format is json") } diags = bundle.Apply(ctx, b, bundle.Seq( diff --git a/cmd/bundle/generate/dashboard.go b/cmd/bundle/generate/dashboard.go index f196bbe62..fa3c91b2a 100644 --- a/cmd/bundle/generate/dashboard.go +++ b/cmd/bundle/generate/dashboard.go @@ -96,7 +96,7 @@ func (d *dashboard) resolveFromPath(ctx context.Context, b *bundle.Bundle) (stri return "", diag.Diagnostics{ { Severity: diag.Error, - Summary: fmt.Sprintf("expected a dashboard, found a %s", found), + Summary: "expected a dashboard, found a " + found, }, } } @@ -188,7 +188,7 @@ func (d *dashboard) saveSerializedDashboard(_ context.Context, b *bundle.Bundle, func (d *dashboard) saveConfiguration(ctx context.Context, b *bundle.Bundle, dashboard *dashboards.Dashboard, key string) error { // Save serialized dashboard definition to the dashboard directory. - dashboardBasename := fmt.Sprintf("%s.lvdash.json", key) + dashboardBasename := key + ".lvdash.json" dashboardPath := filepath.Join(d.dashboardDir, dashboardBasename) err := d.saveSerializedDashboard(ctx, b, dashboard, dashboardPath) if err != nil { @@ -215,7 +215,7 @@ func (d *dashboard) saveConfiguration(ctx context.Context, b *bundle.Bundle, das } // Save the configuration to the resource directory. - resourcePath := filepath.Join(d.resourceDir, fmt.Sprintf("%s.dashboard.yml", key)) + resourcePath := filepath.Join(d.resourceDir, key+".dashboard.yml") saver := yamlsaver.NewSaverWithStyle(map[string]yaml.Style{ "display_name": yaml.DoubleQuotedStyle, }) diff --git a/cmd/bundle/generate/job.go b/cmd/bundle/generate/job.go index 9ac41e3cb..827d270e5 100644 --- a/cmd/bundle/generate/job.go +++ b/cmd/bundle/generate/job.go @@ -85,8 +85,8 @@ func NewGenerateJobCommand() *cobra.Command { return err } - oldFilename := filepath.Join(configDir, fmt.Sprintf("%s.yml", jobKey)) - filename := filepath.Join(configDir, fmt.Sprintf("%s.job.yml", jobKey)) + oldFilename := filepath.Join(configDir, jobKey+".yml") + filename := filepath.Join(configDir, jobKey+".job.yml") // User might continuously run generate command to update their bundle jobs with any changes made in Databricks UI. // Due to changing in the generated file names, we need to first rename existing resource file to the new name. @@ -107,7 +107,7 @@ func NewGenerateJobCommand() *cobra.Command { return err } - cmdio.LogString(ctx, fmt.Sprintf("Job configuration successfully saved to %s", filename)) + cmdio.LogString(ctx, "Job configuration successfully saved to "+filename) return nil } diff --git a/cmd/bundle/generate/pipeline.go b/cmd/bundle/generate/pipeline.go index 910baa45f..863b0b2f7 100644 --- a/cmd/bundle/generate/pipeline.go +++ b/cmd/bundle/generate/pipeline.go @@ -85,8 +85,8 @@ func NewGeneratePipelineCommand() *cobra.Command { return err } - oldFilename := filepath.Join(configDir, fmt.Sprintf("%s.yml", pipelineKey)) - filename := filepath.Join(configDir, fmt.Sprintf("%s.pipeline.yml", pipelineKey)) + oldFilename := filepath.Join(configDir, pipelineKey+".yml") + filename := filepath.Join(configDir, pipelineKey+".pipeline.yml") // User might continuously run generate command to update their bundle jobs with any changes made in Databricks UI. // Due to changing in the generated file names, we need to first rename existing resource file to the new name. @@ -109,7 +109,7 @@ func NewGeneratePipelineCommand() *cobra.Command { return err } - cmdio.LogString(ctx, fmt.Sprintf("Pipeline configuration successfully saved to %s", filename)) + cmdio.LogString(ctx, "Pipeline configuration successfully saved to "+filename) return nil } diff --git a/cmd/bundle/generate/utils.go b/cmd/bundle/generate/utils.go index 8e3764e35..dbfad9438 100644 --- a/cmd/bundle/generate/utils.go +++ b/cmd/bundle/generate/utils.go @@ -126,7 +126,7 @@ func (n *downloader) FlushToDisk(ctx context.Context, force bool) error { return err } - cmdio.LogString(errCtx, fmt.Sprintf("File successfully saved to %s", targetPath)) + cmdio.LogString(errCtx, "File successfully saved to "+targetPath) return reader.Close() }) } diff --git a/cmd/bundle/launch.go b/cmd/bundle/launch.go index 0d2b4233b..3fea839c9 100644 --- a/cmd/bundle/launch.go +++ b/cmd/bundle/launch.go @@ -1,7 +1,7 @@ package bundle import ( - "fmt" + "errors" "github.com/databricks/cli/cmd/root" "github.com/spf13/cobra" @@ -19,7 +19,7 @@ func newLaunchCommand() *cobra.Command { } cmd.RunE = func(cmd *cobra.Command, args []string) error { - return fmt.Errorf("TODO") + return errors.New("TODO") // contents, err := os.ReadFile(args[0]) // if err != nil { // return err diff --git a/cmd/bundle/open.go b/cmd/bundle/open.go index a2ad32fd8..5a26e1ea7 100644 --- a/cmd/bundle/open.go +++ b/cmd/bundle/open.go @@ -44,7 +44,7 @@ func resolveOpenArgument(ctx context.Context, b *bundle.Bundle, args []string) ( } if len(args) < 1 { - return "", fmt.Errorf("expected a KEY of the resource to open") + return "", errors.New("expected a KEY of the resource to open") } return args[0], nil @@ -113,7 +113,7 @@ func newOpenCommand() *cobra.Command { // Confirm that the resource has a URL. url := ref.Resource.GetURL() if url == "" { - return fmt.Errorf("resource does not have a URL associated with it (has it been deployed?)") + return errors.New("resource does not have a URL associated with it (has it been deployed?)") } return browser.OpenURL(url) diff --git a/cmd/bundle/run.go b/cmd/bundle/run.go index 3bcebddd5..df35d7222 100644 --- a/cmd/bundle/run.go +++ b/cmd/bundle/run.go @@ -3,6 +3,7 @@ package bundle import ( "context" "encoding/json" + "errors" "fmt" "github.com/databricks/cli/bundle" @@ -48,7 +49,7 @@ func resolveRunArgument(ctx context.Context, b *bundle.Bundle, args []string) (s } if len(args) < 1 { - return "", nil, fmt.Errorf("expected a KEY of the resource to run") + return "", nil, errors.New("expected a KEY of the resource to run") } return args[0], args[1:], nil diff --git a/cmd/bundle/test.go b/cmd/bundle/test.go index 4d30e727d..794575220 100644 --- a/cmd/bundle/test.go +++ b/cmd/bundle/test.go @@ -1,7 +1,7 @@ package bundle import ( - "fmt" + "errors" "github.com/spf13/cobra" ) @@ -17,7 +17,7 @@ func newTestCommand() *cobra.Command { } cmd.RunE = func(cmd *cobra.Command, args []string) error { - return fmt.Errorf("TODO") + return errors.New("TODO") // results := project.RunPythonOnDev(cmd.Context(), `return 1`) // if results.Failed() { // return results.Err() diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index 3b50cc258..daeb7426d 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -2,6 +2,7 @@ package bundle import ( "encoding/json" + "errors" "fmt" "github.com/databricks/cli/bundle" @@ -39,7 +40,7 @@ func newValidateCommand() *cobra.Command { if err := diags.Error(); err != nil { return diags.Error() } else { - return fmt.Errorf("invariant failed: returned bundle is nil") + return errors.New("invariant failed: returned bundle is nil") } } diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go index 895a5902c..4a6568d06 100644 --- a/cmd/configure/configure.go +++ b/cmd/configure/configure.go @@ -1,6 +1,7 @@ package configure import ( + "errors" "fmt" "github.com/databricks/cli/libs/cmdio" @@ -62,12 +63,12 @@ func configureInteractive(cmd *cobra.Command, flags *configureFlags, cfg *config func configureNonInteractive(cmd *cobra.Command, flags *configureFlags, cfg *config.Config) error { if cfg.Host == "" { - return fmt.Errorf("host must be set in non-interactive mode") + return errors.New("host must be set in non-interactive mode") } // Check presence of cluster ID before reading token to fail fast. if flags.ConfigureCluster && cfg.ClusterID == "" { - return fmt.Errorf("cluster ID must be set in non-interactive mode") + return errors.New("cluster ID must be set in non-interactive mode") } // Read token from stdin if not already set. diff --git a/cmd/configure/host.go b/cmd/configure/host.go index 781c12387..0a454c6d1 100644 --- a/cmd/configure/host.go +++ b/cmd/configure/host.go @@ -1,7 +1,7 @@ package configure import ( - "fmt" + "errors" "net/url" ) @@ -11,10 +11,10 @@ func validateHost(s string) error { return err } if u.Host == "" || u.Scheme != "https" { - return fmt.Errorf("must start with https://") + return errors.New("must start with https://") } if u.Path != "" && u.Path != "/" { - return fmt.Errorf("must use empty path") + return errors.New("must use empty path") } return nil } diff --git a/cmd/labs/github/repositories.go b/cmd/labs/github/repositories.go index 850cdb1cb..afdf7aeb2 100644 --- a/cmd/labs/github/repositories.go +++ b/cmd/labs/github/repositories.go @@ -12,7 +12,7 @@ import ( const repositoryCacheTTL = 24 * time.Hour func NewRepositoryCache(org, cacheDir string) *repositoryCache { - filename := fmt.Sprintf("%s-repositories", org) + filename := org + "-repositories" return &repositoryCache{ cache: localcache.NewLocalCache[Repositories](cacheDir, filename, repositoryCacheTTL), Org: org, diff --git a/cmd/labs/installed.go b/cmd/labs/installed.go index e4249c9ff..9982cc1f0 100644 --- a/cmd/labs/installed.go +++ b/cmd/labs/installed.go @@ -1,6 +1,7 @@ package labs import ( + "errors" "fmt" "github.com/databricks/cli/cmd/labs/project" @@ -49,7 +50,7 @@ func newInstalledCommand() *cobra.Command { }) } if len(info.Projects) == 0 { - return fmt.Errorf("no projects installed") + return errors.New("no projects installed") } return cmdio.Render(ctx, info) }, diff --git a/cmd/labs/localcache/jsonfile.go b/cmd/labs/localcache/jsonfile.go index 6540e4ac2..50ed372f5 100644 --- a/cmd/labs/localcache/jsonfile.go +++ b/cmd/labs/localcache/jsonfile.go @@ -93,7 +93,7 @@ func (r *LocalCache[T]) writeCache(ctx context.Context, data T) (T, error) { } func (r *LocalCache[T]) FileName() string { - return filepath.Join(r.dir, fmt.Sprintf("%s.json", r.name)) + return filepath.Join(r.dir, r.name+".json") } func (r *LocalCache[T]) loadCache() (*cached[T], error) { diff --git a/cmd/labs/localcache/jsonfile_test.go b/cmd/labs/localcache/jsonfile_test.go index 9d42c6179..8172b7d14 100644 --- a/cmd/labs/localcache/jsonfile_test.go +++ b/cmd/labs/localcache/jsonfile_test.go @@ -3,7 +3,6 @@ package localcache import ( "context" "errors" - "fmt" "net/url" "runtime" "testing" @@ -115,7 +114,7 @@ func TestFolderDisappears(t *testing.T) { func TestRefreshFails(t *testing.T) { c := NewLocalCache[int64](t.TempDir(), "time", 1*time.Minute) tick := func() (int64, error) { - return 0, fmt.Errorf("nope") + return 0, errors.New("nope") } ctx := context.Background() _, err := c.Load(ctx, tick) diff --git a/cmd/labs/project/installer.go b/cmd/labs/project/installer.go index 041415964..7d31623bb 100644 --- a/cmd/labs/project/installer.go +++ b/cmd/labs/project/installer.go @@ -175,7 +175,7 @@ func (i *installer) login(ctx context.Context) (*databricks.WorkspaceClient, err return nil, fmt.Errorf("valid: %w", err) } if !i.HasAccountLevelCommands() && cfg.IsAccountClient() { - return nil, fmt.Errorf("got account-level client, but no account-level commands") + return nil, errors.New("got account-level client, but no account-level commands") } lc := &loginConfig{Entrypoint: i.Installer.Entrypoint} w, err := lc.askWorkspace(ctx, cfg) @@ -200,10 +200,10 @@ func (i *installer) downloadLibrary(ctx context.Context) error { libTarget := i.LibDir() // we may support wheels, jars, and golang binaries. but those are not zipballs if i.IsZipball() { - feedback <- fmt.Sprintf("Downloading and unpacking zipball for %s", i.version) + feedback <- "Downloading and unpacking zipball for " + i.version return i.downloadAndUnpackZipball(ctx, libTarget) } - return fmt.Errorf("we only support zipballs for now") + return errors.New("we only support zipballs for now") } func (i *installer) downloadAndUnpackZipball(ctx context.Context, libTarget string) error { @@ -234,7 +234,7 @@ func (i *installer) setupPythonVirtualEnvironment(ctx context.Context, w *databr log.Debugf(ctx, "Detected Python %s at: %s", py.Version, py.Path) venvPath := i.virtualEnvPath(ctx) log.Debugf(ctx, "Creating Python Virtual Environment at: %s", venvPath) - feedback <- fmt.Sprintf("Creating Virtual Environment with Python %s", py.Version) + feedback <- "Creating Virtual Environment with Python " + py.Version _, err = process.Background(ctx, []string{py.Path, "-m", "venv", venvPath}) if err != nil { return fmt.Errorf("create venv: %w", err) @@ -251,8 +251,8 @@ func (i *installer) setupPythonVirtualEnvironment(ctx context.Context, w *databr if !ok { return fmt.Errorf("unsupported runtime: %s", cluster.SparkVersion) } - feedback <- fmt.Sprintf("Installing Databricks Connect v%s", runtimeVersion) - pipSpec := fmt.Sprintf("databricks-connect==%s", runtimeVersion) + feedback <- "Installing Databricks Connect v" + runtimeVersion + pipSpec := "databricks-connect==" + runtimeVersion err = i.installPythonDependencies(ctx, pipSpec) if err != nil { return fmt.Errorf("dbconnect: %w", err) diff --git a/cmd/labs/show.go b/cmd/labs/show.go index c36f0bda3..e8c876d8b 100644 --- a/cmd/labs/show.go +++ b/cmd/labs/show.go @@ -1,7 +1,7 @@ package labs import ( - "fmt" + "errors" "github.com/databricks/cli/cmd/labs/project" "github.com/databricks/cli/cmd/root" @@ -34,7 +34,7 @@ func newShowCommand() *cobra.Command { return err } if len(installed) == 0 { - return fmt.Errorf("no projects found") + return errors.New("no projects found") } name := args[0] for _, v := range installed { diff --git a/cmd/root/auth.go b/cmd/root/auth.go index 07ab48399..49abfd414 100644 --- a/cmd/root/auth.go +++ b/cmd/root/auth.go @@ -26,7 +26,7 @@ type ErrNoWorkspaceProfiles struct { } func (e ErrNoWorkspaceProfiles) Error() string { - return fmt.Sprintf("%s does not contain workspace profiles; please create one by running 'databricks configure'", e.path) + return e.path + " does not contain workspace profiles; please create one by running 'databricks configure'" } type ErrNoAccountProfiles struct { @@ -34,7 +34,7 @@ type ErrNoAccountProfiles struct { } func (e ErrNoAccountProfiles) Error() string { - return fmt.Sprintf("%s does not contain account profiles", e.path) + return e.path + " does not contain account profiles" } func initProfileFlag(cmd *cobra.Command) { @@ -253,7 +253,7 @@ func AskForWorkspaceProfile(ctx context.Context) (string, error) { return profiles[0].Name, nil } i, _, err := cmdio.RunSelect(ctx, &promptui.Select{ - Label: fmt.Sprintf("Workspace profiles defined in %s", path), + Label: "Workspace profiles defined in " + path, Items: profiles, Searcher: profiles.SearchCaseInsensitive, StartInSearchMode: true, @@ -287,7 +287,7 @@ func AskForAccountProfile(ctx context.Context) (string, error) { return profiles[0].Name, nil } i, _, err := cmdio.RunSelect(ctx, &promptui.Select{ - Label: fmt.Sprintf("Account profiles defined in %s", path), + Label: "Account profiles defined in " + path, Items: profiles, Searcher: profiles.SearchCaseInsensitive, StartInSearchMode: true, diff --git a/cmd/root/progress_logger.go b/cmd/root/progress_logger.go index 1458de13a..0cc49b2ac 100644 --- a/cmd/root/progress_logger.go +++ b/cmd/root/progress_logger.go @@ -2,7 +2,7 @@ package root import ( "context" - "fmt" + "errors" "os" "github.com/databricks/cli/libs/cmdio" @@ -37,7 +37,7 @@ func (f *progressLoggerFlag) initializeContext(ctx context.Context) (context.Con if f.log.level.String() != "disabled" && f.log.file.String() == "stderr" && f.ProgressLogFormat == flags.ModeInplace { - return nil, fmt.Errorf("inplace progress logging cannot be used when log-file is stderr") + return nil, errors.New("inplace progress logging cannot be used when log-file is stderr") } format := f.ProgressLogFormat diff --git a/cmd/sync/completion.go b/cmd/sync/completion.go index 422147713..5a65dd528 100644 --- a/cmd/sync/completion.go +++ b/cmd/sync/completion.go @@ -2,7 +2,6 @@ package sync import ( "context" - "fmt" "path" "strings" @@ -52,8 +51,8 @@ func completeRemotePath( } prefixes := []string{ - path.Clean(fmt.Sprintf("/Users/%s", me.UserName)) + "/", - path.Clean(fmt.Sprintf("/Repos/%s", me.UserName)) + "/", + path.Clean("/Users/"+me.UserName) + "/", + path.Clean("/Repos/"+me.UserName) + "/", } validPrefix := false diff --git a/cmd/sync/sync.go b/cmd/sync/sync.go index cd2167a19..dea40f96a 100644 --- a/cmd/sync/sync.go +++ b/cmd/sync/sync.go @@ -2,6 +2,7 @@ package sync import ( "context" + "errors" "flag" "fmt" "io" @@ -29,7 +30,7 @@ type syncFlags struct { func (f *syncFlags) syncOptionsFromBundle(cmd *cobra.Command, args []string, b *bundle.Bundle) (*sync.SyncOptions, error) { if len(args) > 0 { - return nil, fmt.Errorf("SRC and DST are not configurable in the context of a bundle") + return nil, errors.New("SRC and DST are not configurable in the context of a bundle") } opts, err := files.GetSyncOptions(cmd.Context(), bundle.ReadOnly(b)) diff --git a/cmd/workspace/repos/overrides.go b/cmd/workspace/repos/overrides.go index aad38ecc7..561921623 100644 --- a/cmd/workspace/repos/overrides.go +++ b/cmd/workspace/repos/overrides.go @@ -2,6 +2,7 @@ package repos import ( "context" + "errors" "fmt" "strconv" @@ -153,7 +154,7 @@ func repoArgumentToRepoID(ctx context.Context, w *databricks.WorkspaceClient, ar args = append(args, id) } if len(args) != 1 { - return 0, fmt.Errorf("expected to have the id for the corresponding repo to access") + return 0, errors.New("expected to have the id for the corresponding repo to access") } // ---- End copy from cmd/workspace/repos/repos.go ---- diff --git a/cmd/workspace/secrets/put_secret.go b/cmd/workspace/secrets/put_secret.go index f24814f05..b446524f7 100644 --- a/cmd/workspace/secrets/put_secret.go +++ b/cmd/workspace/secrets/put_secret.go @@ -2,7 +2,7 @@ package secrets import ( "encoding/base64" - "fmt" + "errors" "io" "os" @@ -67,7 +67,7 @@ func newPutSecret() *cobra.Command { bytesValueChanged := cmd.Flags().Changed("bytes-value") stringValueChanged := cmd.Flags().Changed("string-value") if bytesValueChanged && stringValueChanged { - return fmt.Errorf("cannot specify both --bytes-value and --string-value") + return errors.New("cannot specify both --bytes-value and --string-value") } if cmd.Flags().Changed("json") { diff --git a/cmd/workspace/workspace/overrides.go b/cmd/workspace/workspace/overrides.go index 216e9b5d8..53438a764 100644 --- a/cmd/workspace/workspace/overrides.go +++ b/cmd/workspace/workspace/overrides.go @@ -36,7 +36,7 @@ func exportOverride(exportCmd *cobra.Command, exportReq *workspace.ExportRequest ctx := cmd.Context() w := root.WorkspaceClient(ctx) if len(args) != 1 { - return fmt.Errorf("expected to have the absolute path of the object or directory") + return errors.New("expected to have the absolute path of the object or directory") } exportReq.Path = args[0] diff --git a/integration/bundle/bind_resource_test.go b/integration/bundle/bind_resource_test.go index 508aa3410..ba10190aa 100644 --- a/integration/bundle/bind_resource_test.go +++ b/integration/bundle/bind_resource_test.go @@ -1,9 +1,9 @@ package bundle_test import ( - "fmt" "os" "path/filepath" + "strconv" "testing" "github.com/databricks/cli/integration/internal/acc" @@ -35,7 +35,7 @@ func TestBindJobToExistingJob(t *testing.T) { }) ctx = env.Set(ctx, "BUNDLE_ROOT", bundleRoot) - c := testcli.NewRunner(t, ctx, "bundle", "deployment", "bind", "foo", fmt.Sprint(jobId), "--auto-approve") + c := testcli.NewRunner(t, ctx, "bundle", "deployment", "bind", "foo", strconv.FormatInt(jobId, 10), "--auto-approve") _, _, err := c.Run() require.NoError(t, err) @@ -53,7 +53,7 @@ func TestBindJobToExistingJob(t *testing.T) { JobId: jobId, }) require.NoError(t, err) - require.Equal(t, job.Settings.Name, fmt.Sprintf("test-job-basic-%s", uniqueId)) + require.Equal(t, job.Settings.Name, "test-job-basic-"+uniqueId) require.Contains(t, job.Settings.Tasks[0].SparkPythonTask.PythonFile, "hello_world.py") c = testcli.NewRunner(t, ctx, "bundle", "deployment", "unbind", "foo") @@ -71,7 +71,7 @@ func TestBindJobToExistingJob(t *testing.T) { JobId: jobId, }) require.NoError(t, err) - require.Equal(t, job.Settings.Name, fmt.Sprintf("test-job-basic-%s", uniqueId)) + require.Equal(t, job.Settings.Name, "test-job-basic-"+uniqueId) require.Contains(t, job.Settings.Tasks[0].SparkPythonTask.PythonFile, "hello_world.py") } @@ -96,7 +96,7 @@ func TestAbortBind(t *testing.T) { // Bind should fail because prompting is not possible. ctx = env.Set(ctx, "BUNDLE_ROOT", bundleRoot) ctx = env.Set(ctx, "TERM", "dumb") - c := testcli.NewRunner(t, ctx, "bundle", "deployment", "bind", "foo", fmt.Sprint(jobId)) + c := testcli.NewRunner(t, ctx, "bundle", "deployment", "bind", "foo", strconv.FormatInt(jobId, 10)) // Expect error suggesting to use --auto-approve _, _, err := c.Run() @@ -114,7 +114,7 @@ func TestAbortBind(t *testing.T) { }) require.NoError(t, err) - require.NotEqual(t, job.Settings.Name, fmt.Sprintf("test-job-basic-%s", uniqueId)) + require.NotEqual(t, job.Settings.Name, "test-job-basic-"+uniqueId) require.Contains(t, job.Settings.Tasks[0].NotebookTask.NotebookPath, "test") } @@ -143,7 +143,7 @@ func TestGenerateAndBind(t *testing.T) { ctx = env.Set(ctx, "BUNDLE_ROOT", bundleRoot) c := testcli.NewRunner(t, ctx, "bundle", "generate", "job", "--key", "test_job_key", - "--existing-job-id", fmt.Sprint(jobId), + "--existing-job-id", strconv.FormatInt(jobId, 10), "--config-dir", filepath.Join(bundleRoot, "resources"), "--source-dir", filepath.Join(bundleRoot, "src")) _, _, err = c.Run() @@ -157,7 +157,7 @@ func TestGenerateAndBind(t *testing.T) { require.Len(t, matches, 1) - c = testcli.NewRunner(t, ctx, "bundle", "deployment", "bind", "test_job_key", fmt.Sprint(jobId), "--auto-approve") + c = testcli.NewRunner(t, ctx, "bundle", "deployment", "bind", "test_job_key", strconv.FormatInt(jobId, 10), "--auto-approve") _, _, err = c.Run() require.NoError(t, err) diff --git a/integration/bundle/clusters_test.go b/integration/bundle/clusters_test.go index 449206208..b94b8365e 100644 --- a/integration/bundle/clusters_test.go +++ b/integration/bundle/clusters_test.go @@ -1,7 +1,6 @@ package bundle_test import ( - "fmt" "testing" "github.com/databricks/cli/integration/internal/acc" @@ -29,7 +28,7 @@ func TestDeployBundleWithCluster(t *testing.T) { t.Cleanup(func() { destroyBundle(t, ctx, root) - cluster, err := wt.W.Clusters.GetByClusterName(ctx, fmt.Sprintf("test-cluster-%s", uniqueId)) + cluster, err := wt.W.Clusters.GetByClusterName(ctx, "test-cluster-"+uniqueId) if err != nil { require.ErrorContains(t, err, "does not exist") } else { @@ -40,7 +39,7 @@ func TestDeployBundleWithCluster(t *testing.T) { deployBundle(t, ctx, root) // Cluster should exists after bundle deployment - cluster, err := wt.W.Clusters.GetByClusterName(ctx, fmt.Sprintf("test-cluster-%s", uniqueId)) + cluster, err := wt.W.Clusters.GetByClusterName(ctx, "test-cluster-"+uniqueId) require.NoError(t, err) require.NotNil(t, cluster) diff --git a/integration/bundle/dashboards_test.go b/integration/bundle/dashboards_test.go index 83b4b8b03..a96b657f6 100644 --- a/integration/bundle/dashboards_test.go +++ b/integration/bundle/dashboards_test.go @@ -40,7 +40,7 @@ func TestDashboards(t *testing.T) { // Load the dashboard by its ID and confirm its display name. dashboard, err := wt.W.Lakeview.GetByDashboardId(ctx, oi.ResourceId) require.NoError(t, err) - assert.Equal(t, fmt.Sprintf("test-dashboard-%s", uniqueID), dashboard.DisplayName) + assert.Equal(t, "test-dashboard-"+uniqueID, dashboard.DisplayName) // Make an out of band modification to the dashboard and confirm that it is detected. _, err = wt.W.Lakeview.Update(ctx, dashboards.UpdateDashboardRequest{ diff --git a/integration/bundle/deploy_to_shared_test.go b/integration/bundle/deploy_to_shared_test.go index b4395f4c6..387d3c67a 100644 --- a/integration/bundle/deploy_to_shared_test.go +++ b/integration/bundle/deploy_to_shared_test.go @@ -1,7 +1,6 @@ package bundle_test import ( - "fmt" "testing" "github.com/databricks/cli/integration/internal/acc" @@ -23,7 +22,7 @@ func TestDeployBasicToSharedWorkspacePath(t *testing.T) { "unique_id": uniqueId, "node_type_id": nodeTypeId, "spark_version": defaultSparkVersion, - "root_path": fmt.Sprintf("/Shared/%s", currentUser.UserName), + "root_path": "/Shared/" + currentUser.UserName, }) t.Cleanup(func() { diff --git a/integration/bundle/empty_bundle_test.go b/integration/bundle/empty_bundle_test.go index 1ab240d13..2c650cbef 100644 --- a/integration/bundle/empty_bundle_test.go +++ b/integration/bundle/empty_bundle_test.go @@ -1,7 +1,6 @@ package bundle_test import ( - "fmt" "os" "path/filepath" "testing" @@ -19,8 +18,7 @@ func TestEmptyBundleDeploy(t *testing.T) { f, err := os.Create(filepath.Join(tmpDir, "databricks.yml")) require.NoError(t, err) - bundleRoot := fmt.Sprintf(`bundle: - name: %s`, uuid.New().String()) + bundleRoot := "bundle:\n name: " + uuid.New().String() _, err = f.WriteString(bundleRoot) require.NoError(t, err) f.Close() diff --git a/integration/bundle/generate_job_test.go b/integration/bundle/generate_job_test.go index b68bb7d61..f3c4c7829 100644 --- a/integration/bundle/generate_job_test.go +++ b/integration/bundle/generate_job_test.go @@ -2,10 +2,10 @@ package bundle_test import ( "context" - "fmt" "os" "path" "path/filepath" + "strconv" "strings" "testing" @@ -37,7 +37,7 @@ func TestGenerateFromExistingJobAndDeploy(t *testing.T) { ctx = env.Set(ctx, "BUNDLE_ROOT", bundleRoot) c := testcli.NewRunner(t, ctx, "bundle", "generate", "job", - "--existing-job-id", fmt.Sprint(jobId), + "--existing-job-id", strconv.FormatInt(jobId, 10), "--config-dir", filepath.Join(bundleRoot, "resources"), "--source-dir", filepath.Join(bundleRoot, "src")) _, _, err := c.Run() @@ -55,7 +55,7 @@ func TestGenerateFromExistingJobAndDeploy(t *testing.T) { require.NoError(t, err) generatedYaml := string(data) require.Contains(t, generatedYaml, "notebook_task:") - require.Contains(t, generatedYaml, fmt.Sprintf("notebook_path: %s", filepath.Join("..", "src", "test.py"))) + require.Contains(t, generatedYaml, "notebook_path: "+filepath.Join("..", "src", "test.py")) require.Contains(t, generatedYaml, "task_key: test") require.Contains(t, generatedYaml, "new_cluster:") require.Contains(t, generatedYaml, "spark_version: 13.3.x-scala2.12") diff --git a/integration/bundle/generate_pipeline_test.go b/integration/bundle/generate_pipeline_test.go index 7843ec0c3..3565ab928 100644 --- a/integration/bundle/generate_pipeline_test.go +++ b/integration/bundle/generate_pipeline_test.go @@ -2,7 +2,6 @@ package bundle_test import ( "context" - "fmt" "os" "path" "path/filepath" @@ -36,7 +35,7 @@ func TestGenerateFromExistingPipelineAndDeploy(t *testing.T) { ctx = env.Set(ctx, "BUNDLE_ROOT", bundleRoot) c := testcli.NewRunner(t, ctx, "bundle", "generate", "pipeline", - "--existing-pipeline-id", fmt.Sprint(pipelineId), + "--existing-pipeline-id", pipelineId, "--config-dir", filepath.Join(bundleRoot, "resources"), "--source-dir", filepath.Join(bundleRoot, "src")) _, _, err := c.Run() @@ -65,9 +64,9 @@ func TestGenerateFromExistingPipelineAndDeploy(t *testing.T) { require.Contains(t, generatedYaml, "libraries:") require.Contains(t, generatedYaml, "- notebook:") - require.Contains(t, generatedYaml, fmt.Sprintf("path: %s", filepath.Join("..", "src", "notebook.py"))) + require.Contains(t, generatedYaml, "path: "+filepath.Join("..", "src", "notebook.py")) require.Contains(t, generatedYaml, "- file:") - require.Contains(t, generatedYaml, fmt.Sprintf("path: %s", filepath.Join("..", "src", "test.py"))) + require.Contains(t, generatedYaml, "path: "+filepath.Join("..", "src", "test.py")) deployBundle(t, ctx, bundleRoot) diff --git a/integration/bundle/init_test.go b/integration/bundle/init_test.go index f5c263ca3..87a3e30e5 100644 --- a/integration/bundle/init_test.go +++ b/integration/bundle/init_test.go @@ -66,7 +66,7 @@ func TestBundleInitOnMlopsStacks(t *testing.T) { // Assert that the README.md file was created contents := testutil.ReadFile(t, filepath.Join(tmpDir2, "repo_name", projectName, "README.md")) - assert.Contains(t, contents, fmt.Sprintf("# %s", projectName)) + assert.Contains(t, contents, "# "+projectName) // Validate the stack testutil.Chdir(t, filepath.Join(tmpDir2, "repo_name", projectName)) diff --git a/integration/cmd/auth/describe_test.go b/integration/cmd/auth/describe_test.go index 41288dce6..f592bc276 100644 --- a/integration/cmd/auth/describe_test.go +++ b/integration/cmd/auth/describe_test.go @@ -2,7 +2,6 @@ package auth_test import ( "context" - "fmt" "testing" "github.com/databricks/cli/internal/testcli" @@ -21,14 +20,14 @@ func TestAuthDescribeSuccess(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, outStr) - require.Contains(t, outStr, fmt.Sprintf("Host: %s", w.Config.Host)) + require.Contains(t, outStr, "Host: "+w.Config.Host) me, err := w.CurrentUser.Me(context.Background()) require.NoError(t, err) - require.Contains(t, outStr, fmt.Sprintf("User: %s", me.UserName)) - require.Contains(t, outStr, fmt.Sprintf("Authenticated with: %s", w.Config.AuthType)) + require.Contains(t, outStr, "User: "+me.UserName) + require.Contains(t, outStr, "Authenticated with: "+w.Config.AuthType) require.Contains(t, outStr, "Current configuration:") - require.Contains(t, outStr, fmt.Sprintf("✓ host: %s", w.Config.Host)) + require.Contains(t, outStr, "✓ host: "+w.Config.Host) require.Contains(t, outStr, "✓ profile: default") } @@ -47,6 +46,6 @@ func TestAuthDescribeFailure(t *testing.T) { w, err := databricks.NewWorkspaceClient(&databricks.Config{}) require.NoError(t, err) - require.Contains(t, outStr, fmt.Sprintf("✓ host: %s", w.Config.Host)) + require.Contains(t, outStr, "✓ host: "+w.Config.Host) require.Contains(t, outStr, "✓ profile: nonexistent (from --profile flag)") } diff --git a/integration/cmd/fs/completion_test.go b/integration/cmd/fs/completion_test.go index 88ce2fcc1..b13bf9d60 100644 --- a/integration/cmd/fs/completion_test.go +++ b/integration/cmd/fs/completion_test.go @@ -2,7 +2,6 @@ package fs_test import ( "context" - "fmt" "strings" "testing" @@ -24,6 +23,6 @@ func TestFsCompletion(t *testing.T) { setupCompletionFile(t, f) stdout, _ := testcli.RequireSuccessfulRun(t, ctx, "__complete", "fs", "ls", tmpDir+"/") - expectedOutput := fmt.Sprintf("%s/dir1/\n:2\n", tmpDir) + expectedOutput := tmpDir + "/dir1/\n:2\n" assert.Equal(t, expectedOutput, stdout.String()) } diff --git a/integration/cmd/jobs/jobs_test.go b/integration/cmd/jobs/jobs_test.go index b6bcfc5b3..7ebc135a3 100644 --- a/integration/cmd/jobs/jobs_test.go +++ b/integration/cmd/jobs/jobs_test.go @@ -3,7 +3,7 @@ package jobs_test import ( "context" "encoding/json" - "fmt" + "strconv" "testing" "github.com/databricks/cli/internal/testcli" @@ -20,5 +20,5 @@ func TestCreateJob(t *testing.T) { var output map[string]int err := json.Unmarshal(stdout.Bytes(), &output) require.NoError(t, err) - testcli.RequireSuccessfulRun(t, ctx, "jobs", "delete", fmt.Sprint(output["job_id"]), "--log-level=debug") + testcli.RequireSuccessfulRun(t, ctx, "jobs", "delete", strconv.Itoa(output["job_id"]), "--log-level=debug") } diff --git a/integration/cmd/sync/sync_test.go b/integration/cmd/sync/sync_test.go index 6f58b7e42..632497054 100644 --- a/integration/cmd/sync/sync_test.go +++ b/integration/cmd/sync/sync_test.go @@ -151,10 +151,7 @@ func (a *syncTest) remoteFileContent(ctx context.Context, relativePath, expected filePath := path.Join(a.remoteRoot, relativePath) // Remove leading "/" so we can use it in the URL. - urlPath := fmt.Sprintf( - "/api/2.0/workspace-files/%s", - strings.TrimLeft(filePath, "/"), - ) + urlPath := "/api/2.0/workspace-files/" + strings.TrimLeft(filePath, "/") apiClient, err := client.New(a.w.Config) require.NoError(a.t, err) diff --git a/integration/cmd/workspace/workspace_test.go b/integration/cmd/workspace/workspace_test.go index 4edbbfc83..c376a87d2 100644 --- a/integration/cmd/workspace/workspace_test.go +++ b/integration/cmd/workspace/workspace_test.go @@ -114,7 +114,7 @@ func TestExportDir(t *testing.T) { require.NoError(t, err) expectedLogs := strings.Join([]string{ - fmt.Sprintf("Exporting files from %s", sourceDir), + "Exporting files from " + sourceDir, fmt.Sprintf("%s -> %s", path.Join(sourceDir, "a/b/c/file-b"), filepath.Join(targetDir, "a/b/c/file-b")), fmt.Sprintf("%s -> %s", path.Join(sourceDir, "file-a"), filepath.Join(targetDir, "file-a")), fmt.Sprintf("%s -> %s", path.Join(sourceDir, "pyNotebook"), filepath.Join(targetDir, "pyNotebook.py")), @@ -185,7 +185,7 @@ func TestImportDir(t *testing.T) { stdout, stderr := testcli.RequireSuccessfulRun(t, ctx, "workspace", "import-dir", "./testdata/import_dir", targetDir, "--log-level=debug") expectedLogs := strings.Join([]string{ - fmt.Sprintf("Importing files from %s", "./testdata/import_dir"), + "Importing files from " + "./testdata/import_dir", fmt.Sprintf("%s -> %s", filepath.FromSlash("a/b/c/file-b"), path.Join(targetDir, "a/b/c/file-b")), fmt.Sprintf("%s -> %s", filepath.FromSlash("file-a"), path.Join(targetDir, "file-a")), fmt.Sprintf("%s -> %s", filepath.FromSlash("jupyterNotebook.ipynb"), path.Join(targetDir, "jupyterNotebook")), diff --git a/integration/internal/acc/fixtures.go b/integration/internal/acc/fixtures.go index cd867fb3a..2367d228f 100644 --- a/integration/internal/acc/fixtures.go +++ b/integration/internal/acc/fixtures.go @@ -45,7 +45,7 @@ func TemporaryDbfsDir(t *WorkspaceT, name ...string) string { // Prefix the name with "integration-test-" to make it easier to identify. name = append([]string{"integration-test-"}, name...) - path := fmt.Sprintf("/tmp/%s", testutil.RandomName(name...)) + path := "/tmp/" + testutil.RandomName(name...) t.Logf("Creating DBFS directory %s", path) err := t.W.Dbfs.MkdirsByPath(ctx, path) diff --git a/integration/python/python_tasks_test.go b/integration/python/python_tasks_test.go index 9411afb13..39b38f890 100644 --- a/integration/python/python_tasks_test.go +++ b/integration/python/python_tasks_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/base64" "encoding/json" - "fmt" "os" "path" "slices" @@ -244,8 +243,8 @@ func prepareDBFSFiles(t *testing.T) *testFiles { return &testFiles{ w: w, pyNotebookPath: path.Join(baseDir, "test.py"), - sparkPythonPath: fmt.Sprintf("dbfs:%s", path.Join(baseDir, "spark.py")), - wheelPath: fmt.Sprintf("dbfs:%s", path.Join(baseDir, "my_test_code-0.0.1-py3-none-any.whl")), + sparkPythonPath: "dbfs:" + path.Join(baseDir, "spark.py"), + wheelPath: "dbfs:" + path.Join(baseDir, "my_test_code-0.0.1-py3-none-any.whl"), } } @@ -268,7 +267,7 @@ func GenerateNotebookTasks(notebookPath string, versions []string, nodeTypeId st tasks := make([]jobs.SubmitTask, 0) for i := range versions { task := jobs.SubmitTask{ - TaskKey: fmt.Sprintf("notebook_%s", strings.ReplaceAll(versions[i], ".", "_")), + TaskKey: "notebook_" + strings.ReplaceAll(versions[i], ".", "_"), NotebookTask: &jobs.NotebookTask{ NotebookPath: notebookPath, }, @@ -289,7 +288,7 @@ func GenerateSparkPythonTasks(notebookPath string, versions []string, nodeTypeId tasks := make([]jobs.SubmitTask, 0) for i := range versions { task := jobs.SubmitTask{ - TaskKey: fmt.Sprintf("spark_%s", strings.ReplaceAll(versions[i], ".", "_")), + TaskKey: "spark_" + strings.ReplaceAll(versions[i], ".", "_"), SparkPythonTask: &jobs.SparkPythonTask{ PythonFile: notebookPath, }, @@ -310,7 +309,7 @@ func GenerateWheelTasks(wheelPath string, versions []string, nodeTypeId string) tasks := make([]jobs.SubmitTask, 0) for i := range versions { task := jobs.SubmitTask{ - TaskKey: fmt.Sprintf("whl_%s", strings.ReplaceAll(versions[i], ".", "_")), + TaskKey: "whl_" + strings.ReplaceAll(versions[i], ".", "_"), PythonWheelTask: &jobs.PythonWheelTask{ PackageName: "my_test_code", EntryPoint: "run", diff --git a/libs/auth/oauth.go b/libs/auth/oauth.go index 026c45468..1037a5a85 100644 --- a/libs/auth/oauth.go +++ b/libs/auth/oauth.go @@ -107,7 +107,7 @@ func (a *PersistentAuth) Load(ctx context.Context) (*oauth2.Token, error) { func (a *PersistentAuth) ProfileName() string { if a.AccountID != "" { - return fmt.Sprintf("ACCOUNT-%s", a.AccountID) + return "ACCOUNT-" + a.AccountID } host := strings.TrimPrefix(a.Host, "https://") split := strings.Split(host, ".") @@ -210,12 +210,12 @@ func (a *PersistentAuth) oidcEndpoints(ctx context.Context) (*oauthAuthorization prefix := a.key() if a.AccountID != "" { return &oauthAuthorizationServer{ - AuthorizationEndpoint: fmt.Sprintf("%s/v1/authorize", prefix), - TokenEndpoint: fmt.Sprintf("%s/v1/token", prefix), + AuthorizationEndpoint: prefix + "/v1/authorize", + TokenEndpoint: prefix + "/v1/token", }, nil } var oauthEndpoints oauthAuthorizationServer - oidc := fmt.Sprintf("%s/oidc/.well-known/oauth-authorization-server", prefix) + oidc := prefix + "/oidc/.well-known/oauth-authorization-server" err := a.http.Do(ctx, "GET", oidc, httpclient.WithResponseUnmarshal(&oauthEndpoints)) if err != nil { return nil, fmt.Errorf("fetch .well-known: %w", err) @@ -247,7 +247,7 @@ func (a *PersistentAuth) oauth2Config(ctx context.Context) (*oauth2.Config, erro TokenURL: endpoints.TokenEndpoint, AuthStyle: oauth2.AuthStyleInParams, }, - RedirectURL: fmt.Sprintf("http://%s", appRedirectAddr), + RedirectURL: "http://" + appRedirectAddr, Scopes: scopes, }, nil } @@ -258,7 +258,7 @@ func (a *PersistentAuth) oauth2Config(ctx context.Context) (*oauth2.Config, erro func (a *PersistentAuth) key() string { a.Host = strings.TrimSuffix(a.Host, "/") if !strings.HasPrefix(a.Host, "http") { - a.Host = fmt.Sprintf("https://%s", a.Host) + a.Host = "https://" + a.Host } if a.AccountID != "" { return fmt.Sprintf("%s/oidc/accounts/%s", a.Host, a.AccountID) diff --git a/libs/auth/oauth_test.go b/libs/auth/oauth_test.go index 837ff4fee..6c3b9bf47 100644 --- a/libs/auth/oauth_test.go +++ b/libs/auth/oauth_test.go @@ -112,7 +112,7 @@ func TestLoadRefresh(t *testing.T) { }, }.ApplyClient(t, func(ctx context.Context, c *client.DatabricksClient) { ctx = useInsecureOAuthHttpClientForTests(ctx) - expectedKey := fmt.Sprintf("%s/oidc/accounts/xyz", c.Config.Host) + expectedKey := c.Config.Host + "/oidc/accounts/xyz" p := &PersistentAuth{ Host: c.Config.Host, AccountID: "xyz", @@ -149,7 +149,7 @@ func TestChallenge(t *testing.T) { }, }.ApplyClient(t, func(ctx context.Context, c *client.DatabricksClient) { ctx = useInsecureOAuthHttpClientForTests(ctx) - expectedKey := fmt.Sprintf("%s/oidc/accounts/xyz", c.Config.Host) + expectedKey := c.Config.Host + "/oidc/accounts/xyz" browserOpened := make(chan string) p := &PersistentAuth{ diff --git a/libs/cmdio/error_event.go b/libs/cmdio/error_event.go index 933f9d0d0..62897995b 100644 --- a/libs/cmdio/error_event.go +++ b/libs/cmdio/error_event.go @@ -1,13 +1,11 @@ package cmdio -import "fmt" - type ErrorEvent struct { Error string `json:"error"` } func (event *ErrorEvent) String() string { - return fmt.Sprintf("Error: %s", event.Error) + return "Error: " + event.Error } func (event *ErrorEvent) IsInplaceSupported() bool { diff --git a/libs/cmdio/logger.go b/libs/cmdio/logger.go index 7bc95e9a5..7edad5bf0 100644 --- a/libs/cmdio/logger.go +++ b/libs/cmdio/logger.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "errors" "fmt" "io" "os" @@ -124,7 +125,7 @@ func splitAtLastNewLine(s string) (string, string) { func (l *Logger) AskSelect(question string, choices []string) (string, error) { if l.Mode == flags.ModeJson { - return "", fmt.Errorf("question prompts are not supported in json mode") + return "", errors.New("question prompts are not supported in json mode") } // Promptui does not support multiline prompts. So we split the question. @@ -140,7 +141,7 @@ func (l *Logger) AskSelect(question string, choices []string) (string, error) { HideHelp: true, Templates: &promptui.SelectTemplates{ Label: "{{.}}: ", - Selected: fmt.Sprintf("%s: {{.}}", last), + Selected: last + ": {{.}}", }, } @@ -153,7 +154,7 @@ func (l *Logger) AskSelect(question string, choices []string) (string, error) { func (l *Logger) Ask(question, defaultVal string) (string, error) { if l.Mode == flags.ModeJson { - return "", fmt.Errorf("question prompts are not supported in json mode") + return "", errors.New("question prompts are not supported in json mode") } // Add default value to question prompt. diff --git a/libs/databrickscfg/cfgpickers/clusters.go b/libs/databrickscfg/cfgpickers/clusters.go index 6ae7d99c6..e27d13690 100644 --- a/libs/databrickscfg/cfgpickers/clusters.go +++ b/libs/databrickscfg/cfgpickers/clusters.go @@ -33,7 +33,7 @@ func GetRuntimeVersion(cluster compute.ClusterDetails) (string, bool) { match = dbrSnapshotVersionRegex.FindStringSubmatch(cluster.SparkVersion) if len(match) > 1 { // we return 14.999 for 14.x-snapshot for semver.Compare() to work properly - return fmt.Sprintf("%s.999", match[1]), true + return match[1] + ".999", true } return "", false } diff --git a/libs/databrickscfg/loader.go b/libs/databrickscfg/loader.go index 12a516c59..84c8398bf 100644 --- a/libs/databrickscfg/loader.go +++ b/libs/databrickscfg/loader.go @@ -19,7 +19,7 @@ var errNoMatchingProfiles = errors.New("no matching config profiles found") type errMultipleProfiles []string func (e errMultipleProfiles) Error() string { - return fmt.Sprintf("multiple profiles matched: %s", strings.Join(e, ", ")) + return "multiple profiles matched: " + strings.Join(e, ", ") } func findMatchingProfile(configFile *config.File, matcher func(*ini.Section) bool) (*ini.Section, error) { diff --git a/libs/dyn/convert/normalize.go b/libs/dyn/convert/normalize.go index 31cd8b6e3..ee26d5afc 100644 --- a/libs/dyn/convert/normalize.go +++ b/libs/dyn/convert/normalize.go @@ -97,7 +97,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen if !pv.IsAnchor() { diags = diags.Append(diag.Diagnostic{ Severity: diag.Warning, - Summary: fmt.Sprintf("unknown field: %s", pk.MustString()), + Summary: "unknown field: " + pk.MustString(), // Show all locations the unknown field is defined at. Locations: pk.Locations(), Paths: []dyn.Path{path}, diff --git a/libs/dyn/jsonloader/json.go b/libs/dyn/jsonloader/json.go index 3f2dc859f..708fc401f 100644 --- a/libs/dyn/jsonloader/json.go +++ b/libs/dyn/jsonloader/json.go @@ -3,6 +3,7 @@ package jsonloader import ( "bytes" "encoding/json" + "errors" "fmt" "io" @@ -20,7 +21,7 @@ func LoadJSON(data []byte, source string) (dyn.Value, error) { value, err := decodeValue(decoder, &offset) if err != nil { if err == io.EOF { - err = fmt.Errorf("unexpected end of JSON input") + err = errors.New("unexpected end of JSON input") } return dyn.InvalidValue, fmt.Errorf("error decoding JSON at %s: %v", value.Location(), err) } @@ -57,7 +58,7 @@ func decodeValue(decoder *json.Decoder, o *Offset) (dyn.Value, error) { } key, ok := keyToken.(string) if !ok { - return invalidValueWithLocation(decoder, o), fmt.Errorf("expected string for object key") + return invalidValueWithLocation(decoder, o), errors.New("expected string for object key") } // Get the offset of the key by subtracting the length of the key and the '"' character diff --git a/libs/dyn/location.go b/libs/dyn/location.go index 961d2f121..d2b2ad596 100644 --- a/libs/dyn/location.go +++ b/libs/dyn/location.go @@ -1,6 +1,7 @@ package dyn import ( + "errors" "fmt" "path/filepath" ) @@ -17,7 +18,7 @@ func (l Location) String() string { func (l Location) Directory() (string, error) { if l.File == "" { - return "", fmt.Errorf("no file in location") + return "", errors.New("no file in location") } return filepath.Dir(l.File), nil diff --git a/libs/dyn/mapping_test.go b/libs/dyn/mapping_test.go index 67144ae55..d0347d22a 100644 --- a/libs/dyn/mapping_test.go +++ b/libs/dyn/mapping_test.go @@ -1,7 +1,7 @@ package dyn_test import ( - "fmt" + "strconv" "testing" "github.com/databricks/cli/libs/dyn" @@ -186,13 +186,13 @@ func TestMappingClone(t *testing.T) { func TestMappingMerge(t *testing.T) { var m1 dyn.Mapping for i := range 10 { - err := m1.Set(dyn.V(fmt.Sprintf("%d", i)), dyn.V(i)) + err := m1.Set(dyn.V(strconv.Itoa(i)), dyn.V(i)) require.NoError(t, err) } var m2 dyn.Mapping for i := 5; i < 15; i++ { - err := m2.Set(dyn.V(fmt.Sprintf("%d", i)), dyn.V(i)) + err := m2.Set(dyn.V(strconv.Itoa(i)), dyn.V(i)) require.NoError(t, err) } diff --git a/libs/dyn/merge/override_test.go b/libs/dyn/merge/override_test.go index ea161d27c..d9d3f3983 100644 --- a/libs/dyn/merge/override_test.go +++ b/libs/dyn/merge/override_test.go @@ -1,7 +1,7 @@ package merge import ( - "fmt" + "errors" "testing" "time" @@ -373,7 +373,7 @@ func TestOverride_Primitive(t *testing.T) { if modified { t.Run(tc.name+" - visitor has error", func(t *testing.T) { - _, visitor := createVisitor(visitorOpts{error: fmt.Errorf("unexpected change in test")}) + _, visitor := createVisitor(visitorOpts{error: errors.New("unexpected change in test")}) _, err := override(dyn.EmptyPath, tc.left, tc.right, visitor) assert.EqualError(t, err, "unexpected change in test") diff --git a/libs/dyn/path_string_test.go b/libs/dyn/path_string_test.go index 0d64bf110..eb1816d7d 100644 --- a/libs/dyn/path_string_test.go +++ b/libs/dyn/path_string_test.go @@ -1,7 +1,7 @@ package dyn_test import ( - "fmt" + "errors" "testing" . "github.com/databricks/cli/libs/dyn" @@ -52,31 +52,31 @@ func TestNewPathFromString(t *testing.T) { }, { input: "foo[123", - err: fmt.Errorf("invalid path: foo[123"), + err: errors.New("invalid path: foo[123"), }, { input: "foo[123]]", - err: fmt.Errorf("invalid path: foo[123]]"), + err: errors.New("invalid path: foo[123]]"), }, { input: "foo[[123]", - err: fmt.Errorf("invalid path: foo[[123]"), + err: errors.New("invalid path: foo[[123]"), }, { input: "foo[[123]]", - err: fmt.Errorf("invalid path: foo[[123]]"), + err: errors.New("invalid path: foo[[123]]"), }, { input: "foo[foo]", - err: fmt.Errorf("invalid path: foo[foo]"), + err: errors.New("invalid path: foo[foo]"), }, { input: "foo..bar", - err: fmt.Errorf("invalid path: foo..bar"), + err: errors.New("invalid path: foo..bar"), }, { input: "foo.bar.", - err: fmt.Errorf("invalid path: foo.bar."), + err: errors.New("invalid path: foo.bar."), }, { // Every component may have a leading dot. @@ -86,7 +86,7 @@ func TestNewPathFromString(t *testing.T) { { // But after an index there must be a dot. input: "foo[1]bar", - err: fmt.Errorf("invalid path: foo[1]bar"), + err: errors.New("invalid path: foo[1]bar"), }, } { p, err := NewPathFromString(tc.input) diff --git a/libs/dyn/visit_map_test.go b/libs/dyn/visit_map_test.go index 3c2908c4b..ad091743d 100644 --- a/libs/dyn/visit_map_test.go +++ b/libs/dyn/visit_map_test.go @@ -1,6 +1,7 @@ package dyn_test import ( + "errors" "fmt" "testing" @@ -71,7 +72,7 @@ func TestMapFuncOnMap(t *testing.T) { }, vbar.AsAny()) // Return error from map function. - ref := fmt.Errorf("error") + ref := errors.New("error") verr, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Key("foo")), func(_ dyn.Path, v dyn.Value) (dyn.Value, error) { return dyn.InvalidValue, ref }) @@ -137,7 +138,7 @@ func TestMapFuncOnSequence(t *testing.T) { assert.Equal(t, []any{42, 45}, v1.AsAny()) // Return error from map function. - ref := fmt.Errorf("error") + ref := errors.New("error") verr, err := dyn.MapByPath(vin, dyn.NewPath(dyn.Index(0)), func(_ dyn.Path, v dyn.Value) (dyn.Value, error) { return dyn.InvalidValue, ref }) @@ -211,7 +212,7 @@ func TestMapForeachOnMapError(t *testing.T) { }) // Check that an error from the map function propagates. - ref := fmt.Errorf("error") + ref := errors.New("error") _, err := dyn.Map(vin, ".", dyn.Foreach(func(_ dyn.Path, v dyn.Value) (dyn.Value, error) { return dyn.InvalidValue, ref })) @@ -255,7 +256,7 @@ func TestMapForeachOnSequenceError(t *testing.T) { }) // Check that an error from the map function propagates. - ref := fmt.Errorf("error") + ref := errors.New("error") _, err := dyn.Map(vin, ".", dyn.Foreach(func(_ dyn.Path, v dyn.Value) (dyn.Value, error) { return dyn.InvalidValue, ref })) diff --git a/libs/dyn/yamlsaver/saver.go b/libs/dyn/yamlsaver/saver.go index 7398e2594..a7838ff36 100644 --- a/libs/dyn/yamlsaver/saver.go +++ b/libs/dyn/yamlsaver/saver.go @@ -123,9 +123,9 @@ func (s *saver) toYamlNodeWithStyle(v dyn.Value, style yaml.Style) (*yaml.Node, } return &yaml.Node{Kind: yaml.ScalarNode, Value: v.MustString(), Style: style}, nil case dyn.KindBool: - return &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprint(v.MustBool()), Style: style}, nil + return &yaml.Node{Kind: yaml.ScalarNode, Value: strconv.FormatBool(v.MustBool()), Style: style}, nil case dyn.KindInt: - return &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprint(v.MustInt()), Style: style}, nil + return &yaml.Node{Kind: yaml.ScalarNode, Value: strconv.FormatInt(v.MustInt(), 10), Style: style}, nil case dyn.KindFloat: return &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprint(v.MustFloat()), Style: style}, nil case dyn.KindTime: diff --git a/libs/errs/aggregate_test.go b/libs/errs/aggregate_test.go index 1af57e099..216276a06 100644 --- a/libs/errs/aggregate_test.go +++ b/libs/errs/aggregate_test.go @@ -1,16 +1,16 @@ package errs import ( - "fmt" + "errors" "testing" "github.com/stretchr/testify/assert" ) func TestFromManyErrors(t *testing.T) { - e1 := fmt.Errorf("Error 1") - e2 := fmt.Errorf("Error 2") - e3 := fmt.Errorf("Error 3") + e1 := errors.New("Error 1") + e2 := errors.New("Error 2") + e3 := errors.New("Error 3") err := FromMany(e1, e2, e3) assert.ErrorIs(t, err, e1) @@ -23,9 +23,9 @@ Error 3`, err.Error()) } func TestFromManyErrorsWihtNil(t *testing.T) { - e1 := fmt.Errorf("Error 1") + e1 := errors.New("Error 1") var e2 error = nil - e3 := fmt.Errorf("Error 3") + e3 := errors.New("Error 3") err := FromMany(e1, e2, e3) assert.ErrorIs(t, err, e1) diff --git a/libs/exec/shell_cmd.go b/libs/exec/shell_cmd.go index 164d09739..057ed06a4 100644 --- a/libs/exec/shell_cmd.go +++ b/libs/exec/shell_cmd.go @@ -2,7 +2,6 @@ package exec import ( "errors" - "fmt" osexec "os/exec" ) @@ -18,7 +17,7 @@ func (s cmdShell) prepare(command string) (*execContext, error) { return &execContext{ executable: s.executable, - args: []string{"/D", "/E:ON", "/V:OFF", "/S", "/C", fmt.Sprintf(`CALL %s`, filename)}, + args: []string{"/D", "/E:ON", "/V:OFF", "/S", "/C", "CALL " + filename}, scriptFile: filename, }, nil } diff --git a/libs/fakefs/fakefs.go b/libs/fakefs/fakefs.go index a8d5eb873..050ee2d6e 100644 --- a/libs/fakefs/fakefs.go +++ b/libs/fakefs/fakefs.go @@ -1,12 +1,12 @@ package fakefs import ( - "fmt" + "errors" "io/fs" "time" ) -var ErrNotImplemented = fmt.Errorf("not implemented") +var ErrNotImplemented = errors.New("not implemented") // DirEntry is a fake implementation of [fs.DirEntry]. type DirEntry struct { diff --git a/libs/filer/fake_filer.go b/libs/filer/fake_filer.go index 76b8bcd94..1e1cbd985 100644 --- a/libs/filer/fake_filer.go +++ b/libs/filer/fake_filer.go @@ -2,7 +2,7 @@ package filer import ( "context" - "fmt" + "errors" "io" "io/fs" "path" @@ -17,7 +17,7 @@ type FakeFiler struct { } func (f *FakeFiler) Write(ctx context.Context, p string, reader io.Reader, mode ...WriteMode) error { - return fmt.Errorf("not implemented") + return errors.New("not implemented") } func (f *FakeFiler) Read(ctx context.Context, p string) (io.ReadCloser, error) { @@ -30,7 +30,7 @@ func (f *FakeFiler) Read(ctx context.Context, p string) (io.ReadCloser, error) { } func (f *FakeFiler) Delete(ctx context.Context, p string, mode ...DeleteMode) error { - return fmt.Errorf("not implemented") + return errors.New("not implemented") } func (f *FakeFiler) ReadDir(ctx context.Context, p string) ([]fs.DirEntry, error) { @@ -59,7 +59,7 @@ func (f *FakeFiler) ReadDir(ctx context.Context, p string) ([]fs.DirEntry, error } func (f *FakeFiler) Mkdir(ctx context.Context, path string) error { - return fmt.Errorf("not implemented") + return errors.New("not implemented") } func (f *FakeFiler) Stat(ctx context.Context, path string) (fs.FileInfo, error) { diff --git a/libs/filer/filer.go b/libs/filer/filer.go index 83dc560cb..372c82929 100644 --- a/libs/filer/filer.go +++ b/libs/filer/filer.go @@ -2,7 +2,6 @@ package filer import ( "context" - "fmt" "io" "io/fs" ) @@ -36,7 +35,7 @@ type FileAlreadyExistsError struct { } func (err FileAlreadyExistsError) Error() string { - return fmt.Sprintf("file already exists: %s", err.path) + return "file already exists: " + err.path } func (err FileAlreadyExistsError) Is(other error) bool { @@ -52,7 +51,7 @@ func (err FileDoesNotExistError) Is(other error) bool { } func (err FileDoesNotExistError) Error() string { - return fmt.Sprintf("file does not exist: %s", err.path) + return "file does not exist: " + err.path } type NoSuchDirectoryError struct { @@ -60,7 +59,7 @@ type NoSuchDirectoryError struct { } func (err NoSuchDirectoryError) Error() string { - return fmt.Sprintf("no such directory: %s", err.path) + return "no such directory: " + err.path } func (err NoSuchDirectoryError) Is(other error) bool { @@ -72,7 +71,7 @@ type NotADirectory struct { } func (err NotADirectory) Error() string { - return fmt.Sprintf("not a directory: %s", err.path) + return "not a directory: " + err.path } func (err NotADirectory) Is(other error) bool { @@ -84,7 +83,7 @@ type NotAFile struct { } func (err NotAFile) Error() string { - return fmt.Sprintf("not a file: %s", err.path) + return "not a file: " + err.path } func (err NotAFile) Is(other error) bool { @@ -96,7 +95,7 @@ type DirectoryNotEmptyError struct { } func (err DirectoryNotEmptyError) Error() string { - return fmt.Sprintf("directory not empty: %s", err.path) + return "directory not empty: " + err.path } func (err DirectoryNotEmptyError) Is(other error) bool { @@ -118,7 +117,7 @@ type PermissionError struct { } func (err PermissionError) Error() string { - return fmt.Sprintf("access denied: %s", err.path) + return "access denied: " + err.path } func (err PermissionError) Is(other error) bool { diff --git a/libs/filer/files_client.go b/libs/filer/files_client.go index 7ea1d0f03..98a534684 100644 --- a/libs/filer/files_client.go +++ b/libs/filer/files_client.go @@ -116,10 +116,7 @@ func (w *FilesClient) urlPath(name string) (string, string, error) { } // The user specified part of the path must be escaped. - urlPath := fmt.Sprintf( - "/api/2.0/fs/files/%s", - url.PathEscape(strings.TrimLeft(absPath, "/")), - ) + urlPath := "/api/2.0/fs/files/" + url.PathEscape(strings.TrimLeft(absPath, "/")) return absPath, urlPath, nil } diff --git a/libs/filer/workspace_files_cache_test.go b/libs/filer/workspace_files_cache_test.go index 8983c5982..a73f415c1 100644 --- a/libs/filer/workspace_files_cache_test.go +++ b/libs/filer/workspace_files_cache_test.go @@ -2,7 +2,7 @@ package filer import ( "context" - "fmt" + "errors" "io" "io/fs" "testing" @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" ) -var errNotImplemented = fmt.Errorf("not implemented") +var errNotImplemented = errors.New("not implemented") type cacheTestFiler struct { calls int diff --git a/libs/flags/json_flag_test.go b/libs/flags/json_flag_test.go index 956a3541c..4bebf8b68 100644 --- a/libs/flags/json_flag_test.go +++ b/libs/flags/json_flag_test.go @@ -1,7 +1,6 @@ package flags import ( - "fmt" "os" "path" "testing" @@ -68,7 +67,7 @@ func TestJsonFlagFile(t *testing.T) { fpath = f.Name() } - err := body.Set(fmt.Sprintf("@%s", fpath)) + err := body.Set("@" + fpath) require.NoError(t, err) diags := body.Unmarshal(&request) diff --git a/libs/flags/output.go b/libs/flags/output.go index 17da144bd..e0c799131 100644 --- a/libs/flags/output.go +++ b/libs/flags/output.go @@ -1,6 +1,7 @@ package flags import ( + "errors" "fmt" "strings" @@ -25,7 +26,7 @@ func (f *Output) Set(s string) error { case `json`, `text`: *f = Output(lower) default: - return fmt.Errorf("accepted arguments are json and text") + return errors.New("accepted arguments are json and text") } return nil } diff --git a/libs/git/reference.go b/libs/git/reference.go index e1126d4f2..6001d70de 100644 --- a/libs/git/reference.go +++ b/libs/git/reference.go @@ -13,8 +13,8 @@ import ( type ReferenceType string var ( - ErrNotAReferencePointer = fmt.Errorf("HEAD does not point to another reference") - ErrNotABranch = fmt.Errorf("HEAD is not a reference to a git branch") + ErrNotAReferencePointer = errors.New("HEAD does not point to another reference") + ErrNotABranch = errors.New("HEAD is not a reference to a git branch") ) const ( diff --git a/libs/git/repository_test.go b/libs/git/repository_test.go index 857df65a9..58a540190 100644 --- a/libs/git/repository_test.go +++ b/libs/git/repository_test.go @@ -1,7 +1,6 @@ package git import ( - "fmt" "os" "path/filepath" "strings" @@ -96,8 +95,7 @@ func (testRepo *testRepository) addOriginUrl(url string) { defer f.Close() _, err = f.WriteString( - fmt.Sprintf(`[remote "origin"] - url = %s`, url)) + "[remote \"origin\"]\n\turl = " + url) require.NoError(testRepo.t, err) // reload config to reflect the remote url diff --git a/libs/git/worktree_test.go b/libs/git/worktree_test.go index 3d620c483..072a9d348 100644 --- a/libs/git/worktree_test.go +++ b/libs/git/worktree_test.go @@ -53,12 +53,12 @@ func TestWorktreeResolveGitDir(t *testing.T) { writeGitCommonDir(t, dir, "../..") t.Run("relative", func(t *testing.T) { - writeGitDir(t, dir, fmt.Sprintf("gitdir: %s", "../.git/worktrees/my_worktree")) + writeGitDir(t, dir, "gitdir: "+"../.git/worktrees/my_worktree") verifyCorrectDirs(t, dir) }) t.Run("absolute", func(t *testing.T) { - writeGitDir(t, dir, fmt.Sprintf("gitdir: %s", filepath.Join(dir, ".git/worktrees/my_worktree"))) + writeGitDir(t, dir, "gitdir: "+filepath.Join(dir, ".git/worktrees/my_worktree")) verifyCorrectDirs(t, dir) }) @@ -77,7 +77,7 @@ func TestWorktreeResolveGitDir(t *testing.T) { func TestWorktreeResolveCommonDir(t *testing.T) { dir := setupWorktree(t) - writeGitDir(t, dir, fmt.Sprintf("gitdir: %s", "../.git/worktrees/my_worktree")) + writeGitDir(t, dir, "gitdir: "+"../.git/worktrees/my_worktree") t.Run("relative", func(t *testing.T) { writeGitCommonDir(t, dir, "../..") diff --git a/libs/jsonschema/instance.go b/libs/jsonschema/instance.go index 4440a2fe2..eb36822a0 100644 --- a/libs/jsonschema/instance.go +++ b/libs/jsonschema/instance.go @@ -2,6 +2,7 @@ package jsonschema import ( "encoding/json" + "errors" "fmt" "os" "slices" @@ -149,7 +150,7 @@ func (s *Schema) validateAnyOf(instance map[string]any) error { // According to the JSON schema RFC, anyOf must contain at least one schema. // https://json-schema.org/draft/2020-12/json-schema-core if len(s.AnyOf) == 0 { - return fmt.Errorf("anyOf must contain at least one schema") + return errors.New("anyOf must contain at least one schema") } for _, anyOf := range s.AnyOf { @@ -158,5 +159,5 @@ func (s *Schema) validateAnyOf(instance map[string]any) error { return nil } } - return fmt.Errorf("instance does not match any of the schemas in anyOf") + return errors.New("instance does not match any of the schemas in anyOf") } diff --git a/libs/jsonschema/utils.go b/libs/jsonschema/utils.go index ff9b88312..bc9339cae 100644 --- a/libs/jsonschema/utils.go +++ b/libs/jsonschema/utils.go @@ -150,7 +150,7 @@ func (e patternMatchError) Error() string { // If custom user error message is defined, return error with the custom message msg := e.FailureMessage if msg == "" { - msg = fmt.Sprintf("Expected to match regex pattern: %s", e.Pattern) + msg = "Expected to match regex pattern: " + e.Pattern } return fmt.Sprintf("invalid value for %s: %q. %s", e.PropertyName, e.PropertyValue, msg) } diff --git a/libs/locker/locker.go b/libs/locker/locker.go index eb59c9f74..aadc50b58 100644 --- a/libs/locker/locker.go +++ b/libs/locker/locker.go @@ -116,14 +116,14 @@ func (locker *Locker) assertLockHeld(ctx context.Context) error { // idempotent function since overwrite is set to true func (locker *Locker) Write(ctx context.Context, pathToFile string, content []byte) error { if !locker.Active { - return fmt.Errorf("failed to put file. deploy lock not held") + return errors.New("failed to put file. deploy lock not held") } return locker.filer.Write(ctx, pathToFile, bytes.NewReader(content), filer.OverwriteIfExists, filer.CreateParentDirectories) } func (locker *Locker) Read(ctx context.Context, path string) (io.ReadCloser, error) { if !locker.Active { - return nil, fmt.Errorf("failed to get file. deploy lock not held") + return nil, errors.New("failed to get file. deploy lock not held") } return locker.filer.Read(ctx, path) } @@ -173,7 +173,7 @@ func (locker *Locker) Lock(ctx context.Context, isForced bool) error { func (locker *Locker) Unlock(ctx context.Context, opts ...UnlockOption) error { if !locker.Active { - return fmt.Errorf("unlock called when lock is not held") + return errors.New("unlock called when lock is not held") } // if allowLockFileNotExist is set, do not throw an error if the lock file does diff --git a/libs/process/background_test.go b/libs/process/background_test.go index 7843375cf..5cc810f5d 100644 --- a/libs/process/background_test.go +++ b/libs/process/background_test.go @@ -4,7 +4,7 @@ import ( "bufio" "bytes" "context" - "fmt" + "errors" "os/exec" "strings" "testing" @@ -101,7 +101,7 @@ func TestBackgroundFails(t *testing.T) { func TestBackgroundFailsOnOption(t *testing.T) { ctx := context.Background() _, err := Background(ctx, []string{"ls", "/dev/null/x"}, func(_ context.Context, c *exec.Cmd) error { - return fmt.Errorf("nope") + return errors.New("nope") }) assert.EqualError(t, err, "nope") } diff --git a/libs/process/stub.go b/libs/process/stub.go index 8ab6fd705..528489098 100644 --- a/libs/process/stub.go +++ b/libs/process/stub.go @@ -168,7 +168,7 @@ func (s *processStub) run(cmd *exec.Cmd) error { } var zeroStub reponseStub if s.reponseStub == zeroStub { - return fmt.Errorf("no default process stub") + return errors.New("no default process stub") } err := s.reponseStub.err if s.reponseStub.stdout != "" { diff --git a/libs/process/stub_test.go b/libs/process/stub_test.go index 81afa3a89..158e8b3a6 100644 --- a/libs/process/stub_test.go +++ b/libs/process/stub_test.go @@ -2,7 +2,7 @@ package process_test import ( "context" - "fmt" + "errors" "os/exec" "testing" @@ -32,7 +32,7 @@ func TestStubOutput(t *testing.T) { func TestStubFailure(t *testing.T) { ctx := context.Background() ctx, stub := process.WithStub(ctx) - stub.WithFailure(fmt.Errorf("nope")) + stub.WithFailure(errors.New("nope")) _, err := process.Background(ctx, []string{"/bin/meeecho", "1"}) require.EqualError(t, err, "/bin/meeecho 1: nope") @@ -51,7 +51,7 @@ func TestStubCallback(t *testing.T) { if err != nil { return err } - return fmt.Errorf("yep") + return errors.New("yep") }) _, err := process.Background(ctx, []string{"/bin/meeecho", "1"}) @@ -70,7 +70,7 @@ func TestStubResponses(t *testing.T) { stub. WithStdoutFor("qux 1", "first"). WithStdoutFor("qux 2", "second"). - WithFailureFor("qux 3", fmt.Errorf("nope")) + WithFailureFor("qux 3", errors.New("nope")) first, err := process.Background(ctx, []string{"/path/is/irrelevant/qux", "1"}) require.NoError(t, err) diff --git a/libs/sync/event.go b/libs/sync/event.go index 05821a477..510a01954 100644 --- a/libs/sync/event.go +++ b/libs/sync/event.go @@ -52,10 +52,10 @@ func (e *EventChanges) IsEmpty() bool { func (e *EventChanges) String() string { var changes []string if len(e.Put) > 0 { - changes = append(changes, fmt.Sprintf("PUT: %s", strings.Join(e.Put, ", "))) + changes = append(changes, "PUT: "+strings.Join(e.Put, ", ")) } if len(e.Delete) > 0 { - changes = append(changes, fmt.Sprintf("DELETE: %s", strings.Join(e.Delete, ", "))) + changes = append(changes, "DELETE: "+strings.Join(e.Delete, ", ")) } return strings.Join(changes, ", ") } @@ -70,7 +70,7 @@ func (e *EventStart) String() string { return "" } - return fmt.Sprintf("Action: %s", e.EventChanges.String()) + return "Action: " + e.EventChanges.String() } func newEventStart(seq int, put, delete []string) Event { @@ -98,9 +98,9 @@ func (e *EventSyncProgress) String() string { switch e.Action { case EventActionPut: - return fmt.Sprintf("Uploaded %s", e.Path) + return "Uploaded " + e.Path case EventActionDelete: - return fmt.Sprintf("Deleted %s", e.Path) + return "Deleted " + e.Path default: panic("invalid action") } diff --git a/libs/sync/path.go b/libs/sync/path.go index 97a908965..87397be4b 100644 --- a/libs/sync/path.go +++ b/libs/sync/path.go @@ -14,7 +14,7 @@ import ( ) func repoPathForPath(me *iam.User, remotePath string) string { - base := path.Clean(fmt.Sprintf("/Repos/%s", me.UserName)) + base := path.Clean("/Repos/" + me.UserName) remotePath = path.Clean(remotePath) for strings.HasPrefix(path.Dir(remotePath), base) && path.Dir(remotePath) != base { remotePath = path.Dir(remotePath) diff --git a/libs/sync/sync.go b/libs/sync/sync.go index dc2c8992a..f13fa934a 100644 --- a/libs/sync/sync.go +++ b/libs/sync/sync.go @@ -2,6 +2,7 @@ package sync import ( "context" + "errors" "fmt" stdsync "sync" "time" @@ -93,7 +94,7 @@ func New(ctx context.Context, opts SyncOptions) (*Sync, error) { // specify the workspace by its resource ID. tracked in: https://databricks.atlassian.net/browse/DECO-194 opts.Host = opts.WorkspaceClient.Config.Host if opts.Host == "" { - return nil, fmt.Errorf("failed to resolve host for snapshot") + return nil, errors.New("failed to resolve host for snapshot") } // For full sync, we start with an empty snapshot. diff --git a/libs/tags/tag.go b/libs/tags/tag.go index 4e9b329ca..64eab947e 100644 --- a/libs/tags/tag.go +++ b/libs/tags/tag.go @@ -1,6 +1,7 @@ package tags import ( + "errors" "fmt" "regexp" "strings" @@ -21,13 +22,13 @@ type tag struct { func (t *tag) ValidateKey(s string) error { if len(s) == 0 { - return fmt.Errorf("key must not be empty") + return errors.New("key must not be empty") } if len(s) > t.keyLength { return fmt.Errorf("key length %d exceeds maximum of %d", len(s), t.keyLength) } if strings.ContainsFunc(s, func(r rune) bool { return !unicode.Is(latin1, r) }) { - return fmt.Errorf("key contains non-latin1 characters") + return errors.New("key contains non-latin1 characters") } if !t.keyPattern.MatchString(s) { return fmt.Errorf("key %q does not match pattern %q", s, t.keyPattern) @@ -40,7 +41,7 @@ func (t *tag) ValidateValue(s string) error { return fmt.Errorf("value length %d exceeds maximum of %d", len(s), t.valueLength) } if strings.ContainsFunc(s, func(r rune) bool { return !unicode.Is(latin1, r) }) { - return fmt.Errorf("value contains non-latin1 characters") + return errors.New("value contains non-latin1 characters") } if !t.valuePattern.MatchString(s) { return fmt.Errorf("value %q does not match pattern %q", s, t.valuePattern) diff --git a/libs/template/config.go b/libs/template/config.go index 8e7695b91..919ba2250 100644 --- a/libs/template/config.go +++ b/libs/template/config.go @@ -189,7 +189,7 @@ func (c *config) promptOnce(property *jsonschema.Schema, name, defaultVal, descr c.values[name], err = property.ParseString(userInput) if err != nil { // Show error and retry if validation fails - cmdio.LogString(c.ctx, fmt.Sprintf("Validation failed: %s", err.Error())) + cmdio.LogString(c.ctx, "Validation failed: "+err.Error()) return retriableError{err: err} } @@ -197,7 +197,7 @@ func (c *config) promptOnce(property *jsonschema.Schema, name, defaultVal, descr err = c.schema.ValidateInstance(c.values) if err != nil { // Show error and retry if validation fails - cmdio.LogString(c.ctx, fmt.Sprintf("Validation failed: %s", err.Error())) + cmdio.LogString(c.ctx, "Validation failed: "+err.Error()) return retriableError{err: err} } return nil diff --git a/libs/template/materialize_test.go b/libs/template/materialize_test.go index f7cd916e3..c9331b43f 100644 --- a/libs/template/materialize_test.go +++ b/libs/template/materialize_test.go @@ -2,7 +2,6 @@ package template import ( "context" - "fmt" "os" "testing" @@ -20,5 +19,5 @@ func TestMaterializeForNonTemplateDirectory(t *testing.T) { // Try to materialize a non-template directory. err = Materialize(ctx, "", os.DirFS(tmpDir), nil) - assert.EqualError(t, err, fmt.Sprintf("not a bundle template: expected to find a template schema file at %s", schemaFileName)) + assert.EqualError(t, err, "not a bundle template: expected to find a template schema file at "+schemaFileName) } diff --git a/libs/template/renderer_test.go b/libs/template/renderer_test.go index 2c14009ff..70c8de12b 100644 --- a/libs/template/renderer_test.go +++ b/libs/template/renderer_test.go @@ -2,7 +2,6 @@ package template import ( "context" - "fmt" "io/fs" "os" "path" @@ -544,7 +543,7 @@ func TestRendererErrorOnConflictingFile(t *testing.T) { out, err := filer.NewLocalClient(tmpDir) require.NoError(t, err) err = r.persistToDisk(ctx, out) - assert.EqualError(t, err, fmt.Sprintf("failed to initialize template, one or more files already exist: %s", "a")) + assert.EqualError(t, err, "failed to initialize template, one or more files already exist: "+"a") } func TestRendererNoErrorOnConflictingFileIfSkipped(t *testing.T) { From e6552231eb3e9dfbf836f2e71e983b88823dbd48 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 7 Jan 2025 17:13:12 +0100 Subject: [PATCH 31/53] Use different cache keys for different jobs (#2091) Otherwise all those jobs compete for the same key and build cache ends up disabled for ubuntu tests. --- .github/workflows/push.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index a4a35420a..db214d67f 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -68,6 +68,10 @@ jobs: - uses: actions/setup-go@v5 with: go-version: 1.23.4 + # Use different schema from regular job, to avoid overwriting the same key + cache-dependency-path: | + go.sum + .golangci.yaml - name: Run go mod tidy run: | go mod tidy @@ -92,6 +96,10 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.23.4 + # Use different schema from regular job, to avoid overwriting the same key + cache-dependency-path: | + go.sum + bundle/internal/schema/*.* - name: Verify that the schema is up to date run: | From 43420d01adf5af7b3ef7368e2ace78bdb7325601 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 7 Jan 2025 18:10:49 +0100 Subject: [PATCH 32/53] Run push.yml periodically with clean cache (#2092) This ensures that our build still works with clean cache and also populates build/test cache, speeding up test runs. --- .github/workflows/push.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index db214d67f..b71b23c4b 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -13,12 +13,26 @@ on: # seed the build cache. branches: - main + schedule: + - cron: '0 0,12 * * *' # Runs at 00:00 and 12:00 UTC daily env: GOTESTSUM_FORMAT: github-actions jobs: + cleanups: + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + steps: + - name: Clean up cache if running on schedule + if: ${{ github.event_name == 'schedule' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh cache delete --all --repo databricks/cli || true + tests: + needs: cleanups runs-on: ${{ matrix.os }} strategy: @@ -61,6 +75,7 @@ jobs: run: make test golangci: + needs: cleanups name: lint runs-on: ubuntu-latest steps: @@ -86,6 +101,7 @@ jobs: args: --timeout=15m validate-bundle-schema: + needs: cleanups runs-on: ubuntu-latest steps: From 02c7df39f6ce69234ac512d9080c64aaea3378c0 Mon Sep 17 00:00:00 2001 From: Gleb Kanterov Date: Wed, 8 Jan 2025 10:29:45 +0100 Subject: [PATCH 33/53] Add 'experimental/python' support (#2052) ## Changes Add `experimental/python` section replacing `experimental/pydabs`. Add 2 new mutators into existing pipeline: - `ApplyPythonMutator(load_resources)` - loads resources from Python code - `ApplyPythonMutator(apply_mutators)` - transforms existing resources defined in Python/YAML Example: ```yaml experimental: python: resources: - "resources:load_resources" mutators: - "mutators:add_email_notifications" ``` ## Tests Unit tests and manually --------- Co-authored-by: Pieter Noordhuis --- bundle/config/experimental.go | 24 ++ .../config/mutator/python/python_mutator.go | 160 +++++++++++--- .../mutator/python/python_mutator_test.go | 207 ++++++++++++------ bundle/internal/schema/annotations.yml | 21 ++ bundle/phases/initialize.go | 2 + bundle/schema/jsonschema.json | 34 +++ 6 files changed, 354 insertions(+), 94 deletions(-) diff --git a/bundle/config/experimental.go b/bundle/config/experimental.go index 4c787168f..7ecac5d7d 100644 --- a/bundle/config/experimental.go +++ b/bundle/config/experimental.go @@ -27,9 +27,33 @@ type Experimental struct { // PyDABs determines whether to load the 'databricks-pydabs' package. // // PyDABs allows to define bundle configuration using Python. + // PyDABs is deprecated use Python instead. PyDABs PyDABs `json:"pydabs,omitempty"` + + // Python configures loading of Python code defined with 'databricks-bundles' package. + Python Python `json:"python,omitempty"` } +type Python struct { + // Resources contains a list of fully qualified function paths to load resources + // defined in Python code. + // + // Example: ["my_project.resources:load_resources"] + Resources []string `json:"resources"` + + // Mutators contains a list of fully qualified function paths to mutator functions. + // + // Example: ["my_project.mutators:add_default_cluster"] + Mutators []string `json:"mutators"` + + // VEnvPath is path to the virtual environment. + // + // If enabled, Python code will execute within this environment. If disabled, + // it defaults to using the Python interpreter available in the current shell. + VEnvPath string `json:"venv_path,omitempty"` +} + +// PyDABs is deprecated use Python instead type PyDABs struct { // Enabled is a flag to enable the feature. Enabled bool `json:"enabled,omitempty"` diff --git a/bundle/config/mutator/python/python_mutator.go b/bundle/config/mutator/python/python_mutator.go index 69c1a5dd6..8009ab243 100644 --- a/bundle/config/mutator/python/python_mutator.go +++ b/bundle/config/mutator/python/python_mutator.go @@ -9,6 +9,7 @@ import ( "io" "os" "path/filepath" + "reflect" "strings" "github.com/databricks/databricks-sdk-go/logger" @@ -40,6 +41,8 @@ const ( // We also open for possibility of appending other sections of bundle configuration, // for example, adding new variables. However, this is not supported yet, and CLI rejects // such changes. + // + // Deprecated, left for backward-compatibility with PyDABs. PythonMutatorPhaseLoad phase = "load" // PythonMutatorPhaseInit is the phase after bundle configuration was loaded, and @@ -59,7 +62,46 @@ const ( // PyDABs can output YAML containing references to variables, and CLI should resolve them. // // Existing resources can't be removed, and CLI rejects such changes. + // + // Deprecated, left for backward-compatibility with PyDABs. PythonMutatorPhaseInit phase = "init" + + // PythonMutatorPhaseLoadResources is the phase in which YAML configuration was loaded. + // + // At this stage, we execute Python code to load resources defined in Python. + // + // During this process, Python code can access: + // - selected deployment target + // - bundle variable values + // - variables provided through CLI argument or environment variables + // + // The following is not available: + // - variables referencing other variables are in unresolved format + // + // Python code can output YAML referencing variables, and CLI should resolve them. + // + // Existing resources can't be removed or modified, and CLI rejects such changes. + // While it's called 'load_resources', this phase is executed in 'init' phase of mutator pipeline. + PythonMutatorPhaseLoadResources phase = "load_resources" + + // PythonMutatorPhaseApplyMutators is the phase in which resources defined in YAML or Python + // are already loaded. + // + // At this stage, we execute Python code to mutate resources defined in YAML or Python. + // + // During this process, Python code can access: + // - selected deployment target + // - bundle variable values + // - variables provided through CLI argument or environment variables + // + // The following is not available: + // - variables referencing other variables are in unresolved format + // + // Python code can output YAML referencing variables, and CLI should resolve them. + // + // Resources can't be added or removed, and CLI rejects such changes. Python code is + // allowed to modify existing resources, but not other parts of bundle configuration. + PythonMutatorPhaseApplyMutators phase = "apply_mutators" ) type pythonMutator struct { @@ -76,18 +118,64 @@ func (m *pythonMutator) Name() string { return fmt.Sprintf("PythonMutator(%s)", m.phase) } -func getExperimental(b *bundle.Bundle) config.Experimental { - if b.Config.Experimental == nil { - return config.Experimental{} +// opts is a common structure for deprecated PyDABs and upcoming Python +// configuration sections +type opts struct { + enabled bool + + venvPath string +} + +// getOpts adapts deprecated PyDABs and upcoming Python configuration +// into a common structure. +func getOpts(b *bundle.Bundle, phase phase) (opts, error) { + experimental := b.Config.Experimental + if experimental == nil { + return opts{}, nil } - return *b.Config.Experimental + // using reflect.DeepEquals in case we add more fields + pydabsEnabled := !reflect.DeepEqual(experimental.PyDABs, config.PyDABs{}) + pythonEnabled := !reflect.DeepEqual(experimental.Python, config.Python{}) + + if pydabsEnabled && pythonEnabled { + return opts{}, errors.New("both experimental/pydabs and experimental/python are enabled, only one can be enabled") + } else if pydabsEnabled { + if !experimental.PyDABs.Enabled { + return opts{}, nil + } + + // don't execute for phases for 'python' section + if phase == PythonMutatorPhaseInit || phase == PythonMutatorPhaseLoad { + return opts{ + enabled: true, + venvPath: experimental.PyDABs.VEnvPath, + }, nil + } else { + return opts{}, nil + } + } else if pythonEnabled { + // don't execute for phases for 'pydabs' section + if phase == PythonMutatorPhaseLoadResources || phase == PythonMutatorPhaseApplyMutators { + return opts{ + enabled: true, + venvPath: experimental.Python.VEnvPath, + }, nil + } else { + return opts{}, nil + } + } else { + return opts{}, nil + } } func (m *pythonMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - experimental := getExperimental(b) + opts, err := getOpts(b, m.phase) + if err != nil { + return diag.Errorf("failed to apply python mutator: %s", err) + } - if !experimental.PyDABs.Enabled { + if !opts.enabled { return nil } @@ -95,8 +183,8 @@ func (m *pythonMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagno var mutateDiags diag.Diagnostics mutateDiagsHasError := errors.New("unexpected error") - err := b.Config.Mutate(func(leftRoot dyn.Value) (dyn.Value, error) { - pythonPath, err := detectExecutable(ctx, experimental.PyDABs.VEnvPath) + err = b.Config.Mutate(func(leftRoot dyn.Value) (dyn.Value, error) { + pythonPath, err := detectExecutable(ctx, opts.venvPath) if err != nil { return dyn.InvalidValue, fmt.Errorf("failed to get Python interpreter path: %w", err) } @@ -137,7 +225,7 @@ func createCacheDir(ctx context.Context) (string, error) { // support the same env variable as in b.CacheDir if tempDir, exists := env.TempDir(ctx); exists { // use 'default' as target name - cacheDir := filepath.Join(tempDir, "default", "pydabs") + cacheDir := filepath.Join(tempDir, "default", "python") err := os.MkdirAll(cacheDir, 0o700) if err != nil { @@ -147,7 +235,7 @@ func createCacheDir(ctx context.Context) (string, error) { return cacheDir, nil } - return os.MkdirTemp("", "-pydabs") + return os.MkdirTemp("", "-python") } func (m *pythonMutator) runPythonMutator(ctx context.Context, cacheDir, rootPath, pythonPath string, root dyn.Value) (dyn.Value, diag.Diagnostics) { @@ -203,7 +291,7 @@ func (m *pythonMutator) runPythonMutator(ctx context.Context, cacheDir, rootPath } // process can fail without reporting errors in diagnostics file or creating it, for instance, - // venv doesn't have PyDABs library installed + // venv doesn't have 'databricks-bundles' library installed if processErr != nil { diagnostic := diag.Diagnostic{ Severity: diag.Error, @@ -226,16 +314,15 @@ func (m *pythonMutator) runPythonMutator(ctx context.Context, cacheDir, rootPath return output, pythonDiagnostics } -const installExplanation = `If using Python wheels, ensure that 'databricks-pydabs' is included in the dependencies, -and that the wheel is installed in the Python environment: +const pythonInstallExplanation = `Ensure that 'databricks-bundles' is installed in Python environment: - $ .venv/bin/pip install -e . + $ .venv/bin/pip install databricks-bundles If using a virtual environment, ensure it is specified as the venv_path property in databricks.yml, or activate the environment before running CLI commands: experimental: - pydabs: + python: venv_path: .venv ` @@ -245,9 +332,9 @@ or activate the environment before running CLI commands: func explainProcessErr(stderr string) string { // implemented in cpython/Lib/runpy.py and portable across Python 3.x, including pypy if strings.Contains(stderr, "Error while finding module specification for 'databricks.bundles.build'") { - summary := color.CyanString("Explanation: ") + "'databricks-pydabs' library is not installed in the Python environment.\n" + summary := color.CyanString("Explanation: ") + "'databricks-bundles' library is not installed in the Python environment.\n" - return stderr + "\n" + summary + "\n" + installExplanation + return stderr + "\n" + summary + "\n" + pythonInstallExplanation } return stderr @@ -277,10 +364,10 @@ func loadOutputFile(rootPath, outputPath string) (dyn.Value, diag.Diagnostics) { // // virtualPath has to stay in rootPath, because locations outside root path are not allowed: // - // Error: path /var/folders/.../pydabs/dist/*.whl is not contained in bundle root path + // Error: path /var/folders/.../python/dist/*.whl is not contained in bundle root path // // for that, we pass virtualPath instead of outputPath as file location - virtualPath, err := filepath.Abs(filepath.Join(rootPath, "__generated_by_pydabs__.yml")) + virtualPath, err := filepath.Abs(filepath.Join(rootPath, "__generated_by_python__.yml")) if err != nil { return dyn.InvalidValue, diag.FromErr(fmt.Errorf("failed to get absolute path: %w", err)) } @@ -334,19 +421,23 @@ func loadDiagnosticsFile(path string) (diag.Diagnostics, error) { func createOverrideVisitor(ctx context.Context, phase phase) (merge.OverrideVisitor, error) { switch phase { case PythonMutatorPhaseLoad: - return createLoadOverrideVisitor(ctx), nil + return createLoadResourcesOverrideVisitor(ctx), nil case PythonMutatorPhaseInit: - return createInitOverrideVisitor(ctx), nil + return createInitOverrideVisitor(ctx, insertResourceModeAllow), nil + case PythonMutatorPhaseLoadResources: + return createLoadResourcesOverrideVisitor(ctx), nil + case PythonMutatorPhaseApplyMutators: + return createInitOverrideVisitor(ctx, insertResourceModeDisallow), nil default: return merge.OverrideVisitor{}, fmt.Errorf("unknown phase: %s", phase) } } -// createLoadOverrideVisitor creates an override visitor for the load phase. +// createLoadResourcesOverrideVisitor creates an override visitor for the load_resources phase. // -// During load, it's only possible to create new resources, and not modify or +// During load_resources, it's only possible to create new resources, and not modify or // delete existing ones. -func createLoadOverrideVisitor(ctx context.Context) merge.OverrideVisitor { +func createLoadResourcesOverrideVisitor(ctx context.Context) merge.OverrideVisitor { resourcesPath := dyn.NewPath(dyn.Key("resources")) jobsPath := dyn.NewPath(dyn.Key("resources"), dyn.Key("jobs")) @@ -385,11 +476,21 @@ func createLoadOverrideVisitor(ctx context.Context) merge.OverrideVisitor { } } +// insertResourceMode controls whether createInitOverrideVisitor allows or disallows inserting new resources. +type insertResourceMode int + +const ( + insertResourceModeDisallow insertResourceMode = iota + insertResourceModeAllow insertResourceMode = iota +) + // createInitOverrideVisitor creates an override visitor for the init phase. // // During the init phase it's possible to create new resources, modify existing // resources, but not delete existing resources. -func createInitOverrideVisitor(ctx context.Context) merge.OverrideVisitor { +// +// If mode is insertResourceModeDisallow, it matching expected behaviour of apply_mutators +func createInitOverrideVisitor(ctx context.Context, mode insertResourceMode) merge.OverrideVisitor { resourcesPath := dyn.NewPath(dyn.Key("resources")) jobsPath := dyn.NewPath(dyn.Key("resources"), dyn.Key("jobs")) @@ -424,6 +525,11 @@ func createInitOverrideVisitor(ctx context.Context) merge.OverrideVisitor { return dyn.InvalidValue, fmt.Errorf("unexpected change at %q (insert)", valuePath.String()) } + insertResource := len(valuePath) == len(jobsPath)+1 + if mode == insertResourceModeDisallow && insertResource { + return dyn.InvalidValue, fmt.Errorf("unexpected change at %q (insert)", valuePath.String()) + } + log.Debugf(ctx, "Insert value at %q", valuePath.String()) return right, nil @@ -441,9 +547,9 @@ func createInitOverrideVisitor(ctx context.Context) merge.OverrideVisitor { } func isOmitemptyDelete(left dyn.Value) bool { - // PyDABs can omit empty sequences/mappings in output, because we don't track them as optional, + // Python output can omit empty sequences/mappings, because we don't track them as optional, // there is no semantic difference between empty and missing, so we keep them as they were before - // PyDABs deleted them. + // Python mutator deleted them. switch left.Kind() { case dyn.KindMap: diff --git a/bundle/config/mutator/python/python_mutator_test.go b/bundle/config/mutator/python/python_mutator_test.go index ff21f8ed9..d51572c8a 100644 --- a/bundle/config/mutator/python/python_mutator_test.go +++ b/bundle/config/mutator/python/python_mutator_test.go @@ -40,13 +40,25 @@ func TestPythonMutator_Name_init(t *testing.T) { assert.Equal(t, "PythonMutator(init)", mutator.Name()) } -func TestPythonMutator_load(t *testing.T) { +func TestPythonMutator_Name_loadResources(t *testing.T) { + mutator := PythonMutator(PythonMutatorPhaseLoadResources) + + assert.Equal(t, "PythonMutator(load_resources)", mutator.Name()) +} + +func TestPythonMutator_Name_applyMutators(t *testing.T) { + mutator := PythonMutator(PythonMutatorPhaseApplyMutators) + + assert.Equal(t, "PythonMutator(apply_mutators)", mutator.Name()) +} + +func TestPythonMutator_loadResources(t *testing.T) { withFakeVEnv(t, ".venv") b := loadYaml("databricks.yml", ` experimental: - pydabs: - enabled: true + python: + resources: ["resources:load_resources"] venv_path: .venv resources: jobs: @@ -60,12 +72,12 @@ func TestPythonMutator_load(t *testing.T) { "-m", "databricks.bundles.build", "--phase", - "load", + "load_resources", }, `{ "experimental": { - "pydabs": { - "enabled": true, + "python": { + "resources": ["resources:load_resources"], "venv_path": ".venv" } }, @@ -83,7 +95,7 @@ func TestPythonMutator_load(t *testing.T) { `{"severity": "warning", "summary": "job doesn't have any tasks", "location": {"file": "src/examples/file.py", "line": 10, "column": 5}}`, ) - mutator := PythonMutator(PythonMutatorPhaseLoad) + mutator := PythonMutator(PythonMutatorPhaseLoadResources) diags := bundle.Apply(ctx, b, mutator) assert.NoError(t, diags.Error()) @@ -109,13 +121,12 @@ func TestPythonMutator_load(t *testing.T) { }, diags[0].Locations) } -func TestPythonMutator_load_disallowed(t *testing.T) { +func TestPythonMutator_loadResources_disallowed(t *testing.T) { withFakeVEnv(t, ".venv") - b := loadYaml("databricks.yml", ` experimental: - pydabs: - enabled: true + python: + resources: ["resources:load_resources"] venv_path: .venv resources: jobs: @@ -129,12 +140,12 @@ func TestPythonMutator_load_disallowed(t *testing.T) { "-m", "databricks.bundles.build", "--phase", - "load", + "load_resources", }, `{ "experimental": { - "pydabs": { - "enabled": true, + "python": { + "resources": ["resources:load_resources"], "venv_path": ".venv" } }, @@ -148,20 +159,20 @@ func TestPythonMutator_load_disallowed(t *testing.T) { } }`, "") - mutator := PythonMutator(PythonMutatorPhaseLoad) + mutator := PythonMutator(PythonMutatorPhaseLoadResources) diag := bundle.Apply(ctx, b, mutator) assert.EqualError(t, diag.Error(), "unexpected change at \"resources.jobs.job0.description\" (insert)") } -func TestPythonMutator_init(t *testing.T) { +func TestPythonMutator_applyMutators(t *testing.T) { withFakeVEnv(t, ".venv") - b := loadYaml("databricks.yml", ` experimental: - pydabs: - enabled: true + python: venv_path: .venv + mutators: + - "mutators:add_description" resources: jobs: job0: @@ -174,13 +185,13 @@ func TestPythonMutator_init(t *testing.T) { "-m", "databricks.bundles.build", "--phase", - "init", + "apply_mutators", }, `{ "experimental": { - "pydabs": { - "enabled": true, - "venv_path": ".venv" + "python": { + "venv_path": ".venv", + "mutators": ["mutators:add_description"] } }, "resources": { @@ -193,7 +204,7 @@ func TestPythonMutator_init(t *testing.T) { } }`, "") - mutator := PythonMutator(PythonMutatorPhaseInit) + mutator := PythonMutator(PythonMutatorPhaseApplyMutators) diag := bundle.Apply(ctx, b, mutator) assert.NoError(t, diag.Error()) @@ -208,12 +219,12 @@ func TestPythonMutator_init(t *testing.T) { require.NoError(t, err) assert.Equal(t, "databricks.yml", name.Location().File) - // 'description' was updated by PyDABs and has location of generated file until + // 'description' was updated by Python code and has location of generated file until // we implement source maps description, err := dyn.GetByPath(v, dyn.MustPathFromString("resources.jobs.job0.description")) require.NoError(t, err) - expectedVirtualPath, err := filepath.Abs("__generated_by_pydabs__.yml") + expectedVirtualPath, err := filepath.Abs("__generated_by_python__.yml") require.NoError(t, err) assert.Equal(t, expectedVirtualPath, description.Location().File) @@ -224,12 +235,12 @@ func TestPythonMutator_init(t *testing.T) { func TestPythonMutator_badOutput(t *testing.T) { withFakeVEnv(t, ".venv") - b := loadYaml("databricks.yml", ` experimental: - pydabs: - enabled: true + python: venv_path: .venv + resources: + - "resources:load_resources" resources: jobs: job0: @@ -242,7 +253,7 @@ func TestPythonMutator_badOutput(t *testing.T) { "-m", "databricks.bundles.build", "--phase", - "load", + "load_resources", }, `{ "resources": { @@ -254,7 +265,7 @@ func TestPythonMutator_badOutput(t *testing.T) { } }`, "") - mutator := PythonMutator(PythonMutatorPhaseLoad) + mutator := PythonMutator(PythonMutatorPhaseLoadResources) diag := bundle.Apply(ctx, b, mutator) assert.EqualError(t, diag.Error(), "unknown field: unknown_property") @@ -270,34 +281,63 @@ func TestPythonMutator_disabled(t *testing.T) { assert.NoError(t, diag.Error()) } -func TestPythonMutator_venvRequired(t *testing.T) { - b := loadYaml("databricks.yml", ` - experimental: - pydabs: - enabled: true`) - - ctx := context.Background() - mutator := PythonMutator(PythonMutatorPhaseLoad) - diag := bundle.Apply(ctx, b, mutator) - - assert.Error(t, diag.Error(), "\"experimental.enable_pydabs\" is enabled, but \"experimental.venv.path\" is not set") -} - func TestPythonMutator_venvNotFound(t *testing.T) { expectedError := fmt.Sprintf("failed to get Python interpreter path: can't find %q, check if virtualenv is created", interpreterPath("bad_path")) b := loadYaml("databricks.yml", ` experimental: - pydabs: - enabled: true - venv_path: bad_path`) + python: + venv_path: bad_path + resources: + - "resources:load_resources"`) - mutator := PythonMutator(PythonMutatorPhaseInit) + mutator := PythonMutator(PythonMutatorPhaseLoadResources) diag := bundle.Apply(context.Background(), b, mutator) assert.EqualError(t, diag.Error(), expectedError) } +func TestGetOps_Python(t *testing.T) { + actual, err := getOpts(&bundle.Bundle{ + Config: config.Root{ + Experimental: &config.Experimental{ + Python: config.Python{ + VEnvPath: ".venv", + Resources: []string{ + "resources:load_resources", + }, + }, + }, + }, + }, PythonMutatorPhaseLoadResources) + + assert.NoError(t, err) + assert.Equal(t, opts{venvPath: ".venv", enabled: true}, actual) +} + +func TestGetOps_PyDABs(t *testing.T) { + actual, err := getOpts(&bundle.Bundle{ + Config: config.Root{ + Experimental: &config.Experimental{ + PyDABs: config.PyDABs{ + VEnvPath: ".venv", + Enabled: true, + }, + }, + }, + }, PythonMutatorPhaseInit) + + assert.NoError(t, err) + assert.Equal(t, opts{venvPath: ".venv", enabled: true}, actual) +} + +func TestGetOps_empty(t *testing.T) { + actual, err := getOpts(&bundle.Bundle{}, PythonMutatorPhaseLoadResources) + + assert.NoError(t, err) + assert.Equal(t, opts{enabled: false}, actual) +} + type createOverrideVisitorTestCase struct { name string updatePath dyn.Path @@ -315,8 +355,8 @@ func TestCreateOverrideVisitor(t *testing.T) { testCases := []createOverrideVisitorTestCase{ { - name: "load: can't change an existing job", - phase: PythonMutatorPhaseLoad, + name: "load_resources: can't change an existing job", + phase: PythonMutatorPhaseLoadResources, updatePath: dyn.MustPathFromString("resources.jobs.job0.name"), deletePath: dyn.MustPathFromString("resources.jobs.job0.name"), insertPath: dyn.MustPathFromString("resources.jobs.job0.name"), @@ -325,32 +365,32 @@ func TestCreateOverrideVisitor(t *testing.T) { updateError: errors.New("unexpected change at \"resources.jobs.job0.name\" (update)"), }, { - name: "load: can't delete an existing job", - phase: PythonMutatorPhaseLoad, + name: "load_resources: can't delete an existing job", + phase: PythonMutatorPhaseLoadResources, deletePath: dyn.MustPathFromString("resources.jobs.job0"), deleteError: errors.New("unexpected change at \"resources.jobs.job0\" (delete)"), }, { - name: "load: can insert 'resources'", - phase: PythonMutatorPhaseLoad, + name: "load_resources: can insert 'resources'", + phase: PythonMutatorPhaseLoadResources, insertPath: dyn.MustPathFromString("resources"), insertError: nil, }, { - name: "load: can insert 'resources.jobs'", - phase: PythonMutatorPhaseLoad, + name: "load_resources: can insert 'resources.jobs'", + phase: PythonMutatorPhaseLoadResources, insertPath: dyn.MustPathFromString("resources.jobs"), insertError: nil, }, { - name: "load: can insert a job", - phase: PythonMutatorPhaseLoad, + name: "load_resources: can insert a job", + phase: PythonMutatorPhaseLoadResources, insertPath: dyn.MustPathFromString("resources.jobs.job0"), insertError: nil, }, { - name: "load: can't change include", - phase: PythonMutatorPhaseLoad, + name: "load_resources: can't change include", + phase: PythonMutatorPhaseLoadResources, deletePath: dyn.MustPathFromString("include[0]"), insertPath: dyn.MustPathFromString("include[0]"), updatePath: dyn.MustPathFromString("include[0]"), @@ -402,6 +442,40 @@ func TestCreateOverrideVisitor(t *testing.T) { insertError: errors.New("unexpected change at \"include[0]\" (insert)"), updateError: errors.New("unexpected change at \"include[0]\" (update)"), }, + { + name: "apply_mutators: can't delete an existing job", + phase: PythonMutatorPhaseInit, + deletePath: dyn.MustPathFromString("resources.jobs.job0"), + deleteError: errors.New("unexpected change at \"resources.jobs.job0\" (delete)"), + }, + { + name: "apply_mutators: can insert 'resources'", + phase: PythonMutatorPhaseApplyMutators, + insertPath: dyn.MustPathFromString("resources"), + insertError: nil, + }, + { + name: "apply_mutators: can insert 'resources.jobs'", + phase: PythonMutatorPhaseApplyMutators, + insertPath: dyn.MustPathFromString("resources.jobs"), + insertError: nil, + }, + { + name: "apply_mutators: can't insert a job", + phase: PythonMutatorPhaseApplyMutators, + insertPath: dyn.MustPathFromString("resources.jobs.job0"), + insertError: errors.New("unexpected change at \"resources.jobs.job0\" (insert)"), + }, + { + name: "apply_mutators: can't change include", + phase: PythonMutatorPhaseApplyMutators, + deletePath: dyn.MustPathFromString("include[0]"), + insertPath: dyn.MustPathFromString("include[0]"), + updatePath: dyn.MustPathFromString("include[0]"), + deleteError: errors.New("unexpected change at \"include[0]\" (delete)"), + insertError: errors.New("unexpected change at \"include[0]\" (insert)"), + updateError: errors.New("unexpected change at \"include[0]\" (update)"), + }, } for _, tc := range testCases { @@ -459,9 +533,9 @@ type overrideVisitorOmitemptyTestCase struct { } func TestCreateOverrideVisitor_omitempty(t *testing.T) { - // PyDABs can omit empty sequences/mappings in output, because we don't track them as optional, + // Python output can omit empty sequences/mappings in output, because we don't track them as optional, // there is no semantic difference between empty and missing, so we keep them as they were before - // PyDABs deleted them. + // Python code deleted them. allPhases := []phase{PythonMutatorPhaseLoad, PythonMutatorPhaseInit} location := dyn.Location{ @@ -568,18 +642,17 @@ func TestExplainProcessErr(t *testing.T) { stderr := "/home/test/.venv/bin/python3: Error while finding module specification for 'databricks.bundles.build' (ModuleNotFoundError: No module named 'databricks')\n" expected := `/home/test/.venv/bin/python3: Error while finding module specification for 'databricks.bundles.build' (ModuleNotFoundError: No module named 'databricks') -Explanation: 'databricks-pydabs' library is not installed in the Python environment. +Explanation: 'databricks-bundles' library is not installed in the Python environment. -If using Python wheels, ensure that 'databricks-pydabs' is included in the dependencies, -and that the wheel is installed in the Python environment: +Ensure that 'databricks-bundles' is installed in Python environment: - $ .venv/bin/pip install -e . + $ .venv/bin/pip install databricks-bundles If using a virtual environment, ensure it is specified as the venv_path property in databricks.yml, or activate the environment before running CLI commands: experimental: - pydabs: + python: venv_path: .venv ` diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index 84f6753e3..5283a431b 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -69,6 +69,9 @@ github.com/databricks/cli/bundle/config.Experimental: "pydabs": "description": |- The PyDABs configuration. + "python": + "description": |- + Configures loading of Python code defined with 'databricks-bundles' package. "python_wheel_wrapper": "description": |- Whether to use a Python wheel wrapper @@ -125,6 +128,24 @@ github.com/databricks/cli/bundle/config.PyDABs: "venv_path": "description": |- The Python virtual environment path +github.com/databricks/cli/bundle/config.Python: + "mutators": + "description": |- + Mutators contains a list of fully qualified function paths to mutator functions. + + Example: ["my_project.mutators:add_default_cluster"] + "resources": + "description": |- + Resources contains a list of fully qualified function paths to load resources + defined in Python code. + + Example: ["my_project.resources:load_resources"] + "venv_path": + "description": |- + VEnvPath is path to the virtual environment. + + If enabled, Python code will execute within this environment. If disabled, + it defaults to using the Python interpreter available in the current shell. github.com/databricks/cli/bundle/config.Resources: "clusters": "description": |- diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index 6fa0e5fed..f0cbc00c2 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -55,6 +55,8 @@ func Initialize() bundle.Mutator { // ResolveVariableReferencesInComplexVariables and ResolveVariableReferences. // See what is expected in PythonMutatorPhaseInit doc pythonmutator.PythonMutator(pythonmutator.PythonMutatorPhaseInit), + pythonmutator.PythonMutator(pythonmutator.PythonMutatorPhaseLoadResources), + pythonmutator.PythonMutator(pythonmutator.PythonMutatorPhaseApplyMutators), mutator.ResolveVariableReferencesInLookup(), mutator.ResolveResourceReferences(), mutator.ResolveVariableReferencesInComplexVariables(), diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 9a352ebb2..2f78ffcca 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -1100,6 +1100,10 @@ "description": "The PyDABs configuration.", "$ref": "#/$defs/github.com/databricks/cli/bundle/config.PyDABs" }, + "python": { + "description": "Configures loading of Python code defined with 'databricks-bundles' package.", + "$ref": "#/$defs/github.com/databricks/cli/bundle/config.Python" + }, "python_wheel_wrapper": { "description": "Whether to use a Python wheel wrapper", "$ref": "#/$defs/bool" @@ -1234,6 +1238,36 @@ } ] }, + "config.Python": { + "oneOf": [ + { + "type": "object", + "properties": { + "mutators": { + "description": "Mutators contains a list of fully qualified function paths to mutator functions.\n\nExample: [\"my_project.mutators:add_default_cluster\"]", + "$ref": "#/$defs/slice/string" + }, + "resources": { + "description": "Resources contains a list of fully qualified function paths to load resources\ndefined in Python code.\n\nExample: [\"my_project.resources:load_resources\"]", + "$ref": "#/$defs/slice/string" + }, + "venv_path": { + "description": "VEnvPath is path to the virtual environment.\n\nIf enabled, Python code will execute within this environment. If disabled,\nit defaults to using the Python interpreter available in the current shell.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "resources", + "mutators" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "config.Resources": { "oneOf": [ { From 8fd793b605f3fe4f2b25f76dfd6afcc6ae7985a3 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 8 Jan 2025 12:59:22 +0100 Subject: [PATCH 34/53] Clean up TestMain from integration tests to fix caching (#2090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes - Remove TestMain from integration tests and related checks. - This fixes "go test" caching for integration tests. The test_main.go files were added in https://github.com/databricks/cli/pull/2009 to make sure integration tests are not run as part of go test ./.... We recommend running make test to run tests, which includes the packages to test (and excludes integration). ## Tests To test that caching works I ran a test twice: ``` + CLOUD_ENV=aws + go test --timeout 3h -v -run TestDefaultPython/3.9 ./integration/bundle/ … PASS ok github.com/databricks/cli/integration/bundle (cached) ``` --- integration/assumptions/main_test.go | 13 -- integration/bundle/main_test.go | 13 -- integration/cmd/alerts/main_test.go | 13 -- integration/cmd/api/main_test.go | 13 -- integration/cmd/auth/main_test.go | 13 -- integration/cmd/clusters/main_test.go | 13 -- integration/cmd/fs/main_test.go | 13 -- integration/cmd/jobs/main_test.go | 13 -- integration/cmd/main_test.go | 13 -- integration/cmd/repos/main_test.go | 13 -- integration/cmd/secrets/main_test.go | 13 -- .../cmd/storage_credentials/main_test.go | 13 -- integration/cmd/sync/main_test.go | 13 -- integration/cmd/version/main_test.go | 13 -- integration/cmd/workspace/main_test.go | 13 -- integration/enforce_convention_test.go | 116 ------------------ integration/internal/main.go | 22 ---- integration/libs/filer/main_test.go | 13 -- integration/libs/git/main_test.go | 13 -- integration/libs/locker/main_test.go | 13 -- integration/libs/tags/main_test.go | 13 -- integration/python/main_test.go | 13 -- 22 files changed, 398 deletions(-) delete mode 100644 integration/assumptions/main_test.go delete mode 100644 integration/bundle/main_test.go delete mode 100644 integration/cmd/alerts/main_test.go delete mode 100644 integration/cmd/api/main_test.go delete mode 100644 integration/cmd/auth/main_test.go delete mode 100644 integration/cmd/clusters/main_test.go delete mode 100644 integration/cmd/fs/main_test.go delete mode 100644 integration/cmd/jobs/main_test.go delete mode 100644 integration/cmd/main_test.go delete mode 100644 integration/cmd/repos/main_test.go delete mode 100644 integration/cmd/secrets/main_test.go delete mode 100644 integration/cmd/storage_credentials/main_test.go delete mode 100644 integration/cmd/sync/main_test.go delete mode 100644 integration/cmd/version/main_test.go delete mode 100644 integration/cmd/workspace/main_test.go delete mode 100644 integration/enforce_convention_test.go delete mode 100644 integration/internal/main.go delete mode 100644 integration/libs/filer/main_test.go delete mode 100644 integration/libs/git/main_test.go delete mode 100644 integration/libs/locker/main_test.go delete mode 100644 integration/libs/tags/main_test.go delete mode 100644 integration/python/main_test.go diff --git a/integration/assumptions/main_test.go b/integration/assumptions/main_test.go deleted file mode 100644 index be2761385..000000000 --- a/integration/assumptions/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package assumptions_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/bundle/main_test.go b/integration/bundle/main_test.go deleted file mode 100644 index 1c44d0aaf..000000000 --- a/integration/bundle/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package bundle_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/alerts/main_test.go b/integration/cmd/alerts/main_test.go deleted file mode 100644 index 6987ade02..000000000 --- a/integration/cmd/alerts/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package alerts_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/api/main_test.go b/integration/cmd/api/main_test.go deleted file mode 100644 index 70d021790..000000000 --- a/integration/cmd/api/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package api_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/auth/main_test.go b/integration/cmd/auth/main_test.go deleted file mode 100644 index 97b1d740b..000000000 --- a/integration/cmd/auth/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package auth_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/clusters/main_test.go b/integration/cmd/clusters/main_test.go deleted file mode 100644 index ccd5660e7..000000000 --- a/integration/cmd/clusters/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package clusters_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/fs/main_test.go b/integration/cmd/fs/main_test.go deleted file mode 100644 index b9402f0b2..000000000 --- a/integration/cmd/fs/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package fs_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/jobs/main_test.go b/integration/cmd/jobs/main_test.go deleted file mode 100644 index 46369a526..000000000 --- a/integration/cmd/jobs/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package jobs_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/main_test.go b/integration/cmd/main_test.go deleted file mode 100644 index a1a5586b6..000000000 --- a/integration/cmd/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package cmd_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/repos/main_test.go b/integration/cmd/repos/main_test.go deleted file mode 100644 index 7eaa174bc..000000000 --- a/integration/cmd/repos/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package repos_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/secrets/main_test.go b/integration/cmd/secrets/main_test.go deleted file mode 100644 index a44d30671..000000000 --- a/integration/cmd/secrets/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package secrets_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/storage_credentials/main_test.go b/integration/cmd/storage_credentials/main_test.go deleted file mode 100644 index 14d00d966..000000000 --- a/integration/cmd/storage_credentials/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package storage_credentials_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/sync/main_test.go b/integration/cmd/sync/main_test.go deleted file mode 100644 index 8d9f3ca25..000000000 --- a/integration/cmd/sync/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package sync_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/version/main_test.go b/integration/cmd/version/main_test.go deleted file mode 100644 index 4aa5e046a..000000000 --- a/integration/cmd/version/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package version_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/cmd/workspace/main_test.go b/integration/cmd/workspace/main_test.go deleted file mode 100644 index 40d140eac..000000000 --- a/integration/cmd/workspace/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package workspace_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/enforce_convention_test.go b/integration/enforce_convention_test.go deleted file mode 100644 index cc822a6a3..000000000 --- a/integration/enforce_convention_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package integration - -import ( - "go/parser" - "go/token" - "os" - "path/filepath" - "strings" - "testing" - "text/template" - - "golang.org/x/exp/maps" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type packageInfo struct { - Name string - Files []string -} - -func enumeratePackages(t *testing.T) map[string]packageInfo { - pkgmap := make(map[string]packageInfo) - err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // Skip files. - if !info.IsDir() { - return nil - } - - // Skip the root directory and the "internal" directory. - if path == "." || strings.HasPrefix(path, "internal") { - return nil - } - - fset := token.NewFileSet() - pkgs, err := parser.ParseDir(fset, path, nil, parser.ParseComments) - require.NoError(t, err) - if len(pkgs) == 0 { - return nil - } - - // Expect one package per directory. - require.Len(t, pkgs, 1, "Directory %s contains more than one package", path) - v := maps.Values(pkgs)[0] - - // Record the package. - pkgmap[path] = packageInfo{ - Name: v.Name, - Files: maps.Keys(v.Files), - } - return nil - }) - require.NoError(t, err) - return pkgmap -} - -// TestEnforcePackageNames checks that all integration test package names use the "_test" suffix. -// We enforce this package name to avoid package name aliasing. -func TestEnforcePackageNames(t *testing.T) { - pkgmap := enumeratePackages(t) - for _, pkg := range pkgmap { - assert.True(t, strings.HasSuffix(pkg.Name, "_test"), "Package name %s does not end with _test", pkg.Name) - } -} - -var mainTestTemplate = template.Must(template.New("main_test").Parse( - `package {{.Name}} - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} -`)) - -func TestEnforceMainTest(t *testing.T) { - pkgmap := enumeratePackages(t) - for dir, pkg := range pkgmap { - found := false - for _, file := range pkg.Files { - if filepath.Base(file) == "main_test.go" { - found = true - break - } - } - - // Expect a "main_test.go" file in each package. - assert.True(t, found, "Directory %s does not contain a main_test.go file", dir) - } -} - -func TestWriteMainTest(t *testing.T) { - t.Skip("Uncomment to write main_test.go files") - - pkgmap := enumeratePackages(t) - for dir, pkg := range pkgmap { - // Write a "main_test.go" file to the package. - // This file is required to run the integration tests. - f, err := os.Create(filepath.Join(dir, "main_test.go")) - require.NoError(t, err) - defer f.Close() - err = mainTestTemplate.Execute(f, pkg) - require.NoError(t, err) - } -} diff --git a/integration/internal/main.go b/integration/internal/main.go deleted file mode 100644 index 6aa2a4c93..000000000 --- a/integration/internal/main.go +++ /dev/null @@ -1,22 +0,0 @@ -package internal - -import ( - "fmt" - "os" - "testing" - - "github.com/databricks/cli/integration/internal/acc" -) - -// Main is the entry point for integration tests. -// We use this for all integration tests defined in this subtree to ensure -// they are not inadvertently executed when calling `go test ./...`. -func Main(m *testing.M) { - value := os.Getenv("CLOUD_ENV") - if value == "" && !acc.IsInDebug() { - fmt.Println("CLOUD_ENV is not set, skipping integration tests") - return - } - - m.Run() -} diff --git a/integration/libs/filer/main_test.go b/integration/libs/filer/main_test.go deleted file mode 100644 index ca866d952..000000000 --- a/integration/libs/filer/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package filer_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/libs/git/main_test.go b/integration/libs/git/main_test.go deleted file mode 100644 index 5d68e0851..000000000 --- a/integration/libs/git/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package git_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/libs/locker/main_test.go b/integration/libs/locker/main_test.go deleted file mode 100644 index 33a883768..000000000 --- a/integration/libs/locker/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package locker_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/libs/tags/main_test.go b/integration/libs/tags/main_test.go deleted file mode 100644 index 4eaf54a20..000000000 --- a/integration/libs/tags/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package tags_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} diff --git a/integration/python/main_test.go b/integration/python/main_test.go deleted file mode 100644 index b35da21e1..000000000 --- a/integration/python/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package python_test - -import ( - "testing" - - "github.com/databricks/cli/integration/internal" -) - -// TestMain is the entrypoint executed by the test runner. -// See [internal.Main] for prerequisites for running integration tests. -func TestMain(m *testing.M) { - internal.Main(m) -} From 185bbd28e476206da31c0db2d17b9e3e89d6697b Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 8 Jan 2025 13:41:08 +0100 Subject: [PATCH 35/53] Add acceptance tests (#2081) ## Changes - New kind of test is added - acceptance tests. See acceptance/README.md for explanation. - A few tests are converted to acceptance tests by moving databricks.yml to acceptance/ and adding corresponding script files. As these tests run against compiled binary and can capture full output of the command, they can be useful to support major changes such as refactoring internal logging / diagnostics or complex variable interpolation. These are currently run as part of 'make test' but the intention is to run them as part of integration tests as well. ### Benefits - Full binary is tested, exactly as users get it. - We're not testing custom set of mutators like many existing tests. - Not mocking anything, real SDK is used (although the HTTP endpoint is not a real Databricks env). - Easy to maintain: output can be updated automatically. - Can easily set up external env, such as env vars, CLI args, .databrickscfg location etc. ### Gaps The tests currently share the test server and there is global place to define handlers. We should have a way for tests to override / add new handlers. ## Tests I manually checked that output of new acceptance tests matches previous asserts. --- Makefile | 2 +- acceptance/README.md | 19 ++ acceptance/acceptance_test.go | 302 ++++++++++++++++++ acceptance/build/.gitignore | 1 + .../bundle/override}/clusters/databricks.yml | 3 - .../bundle/override/clusters/output.txt | 33 ++ acceptance/bundle/override/clusters/script | 2 + .../override/job_cluster}/databricks.yml | 3 - .../bundle/override/job_cluster/output.txt | 30 ++ acceptance/bundle/override/job_cluster/script | 2 + .../bundle/override/job_tasks}/databricks.yml | 3 - .../bundle/override/job_tasks/output.txt | 68 ++++ acceptance/bundle/override/job_tasks/script | 2 + .../override/merge-string-map/databricks.yml | 13 + .../override/merge-string-map/output.txt | 23 ++ .../bundle/override/merge-string-map/script | 2 + .../override/pipeline_cluster}/databricks.yml | 3 - .../override/pipeline_cluster/output.txt | 44 +++ .../bundle/override/pipeline_cluster/script | 2 + .../complex-transitive/databricks.yml | 19 ++ .../variables/complex-transitive/output.txt | 3 + .../variables/complex-transitive/script | 2 + .../bundle}/variables/complex/databricks.yml | 1 + .../bundle/variables/complex/out.default.json | 124 +++++++ .../bundle/variables/complex/out.dev.json | 118 +++++++ acceptance/bundle/variables/complex/script | 4 + .../complex_multiple_files/databricks.yml | 0 .../complex_multiple_files/output.txt | 159 +++++++++ .../variables/complex_multiple_files/script | 1 + .../variables/clusters.yml | 0 .../bundle}/variables/empty/databricks.yml | 0 acceptance/bundle/variables/empty/output.txt | 11 + acceptance/bundle/variables/empty/script | 1 + .../variables/env_overrides/databricks.yml | 0 .../bundle/variables/env_overrides/output.txt | 40 +++ .../bundle/variables/env_overrides/script | 6 + .../bundle}/variables/vanilla/databricks.yml | 0 .../bundle/variables/vanilla/output.txt | 16 + acceptance/bundle/variables/vanilla/script | 2 + .../databricks.yml | 0 .../variable_overrides_in_target/output.txt | 84 +++++ .../variable_overrides_in_target/script | 4 + .../without_definition/databricks.yml | 2 + .../variables/without_definition/output.txt | 4 + .../variables/without_definition/script | 1 + acceptance/help/output.txt | 143 +++++++++ acceptance/help/script | 1 + acceptance/script.cleanup | 1 + acceptance/script.prepare | 36 +++ acceptance/server_test.go | 129 ++++++++ bundle/tests/clusters_test.go | 36 --- bundle/tests/complex_variables_test.go | 108 ------- bundle/tests/override_job_cluster_test.go | 29 -- bundle/tests/override_job_tasks_test.go | 39 --- .../tests/override_pipeline_cluster_test.go | 29 -- bundle/tests/variables_test.go | 206 ------------ libs/env/context.go | 6 +- libs/testdiff/testdiff.go | 4 +- 58 files changed, 1462 insertions(+), 464 deletions(-) create mode 100644 acceptance/README.md create mode 100644 acceptance/acceptance_test.go create mode 100644 acceptance/build/.gitignore rename {bundle/tests => acceptance/bundle/override}/clusters/databricks.yml (92%) create mode 100644 acceptance/bundle/override/clusters/output.txt create mode 100644 acceptance/bundle/override/clusters/script rename {bundle/tests/override_job_cluster => acceptance/bundle/override/job_cluster}/databricks.yml (91%) create mode 100644 acceptance/bundle/override/job_cluster/output.txt create mode 100644 acceptance/bundle/override/job_cluster/script rename {bundle/tests/override_job_tasks => acceptance/bundle/override/job_tasks}/databricks.yml (94%) create mode 100644 acceptance/bundle/override/job_tasks/output.txt create mode 100644 acceptance/bundle/override/job_tasks/script create mode 100644 acceptance/bundle/override/merge-string-map/databricks.yml create mode 100644 acceptance/bundle/override/merge-string-map/output.txt create mode 100644 acceptance/bundle/override/merge-string-map/script rename {bundle/tests/override_pipeline_cluster => acceptance/bundle/override/pipeline_cluster}/databricks.yml (90%) create mode 100644 acceptance/bundle/override/pipeline_cluster/output.txt create mode 100644 acceptance/bundle/override/pipeline_cluster/script create mode 100644 acceptance/bundle/variables/complex-transitive/databricks.yml create mode 100644 acceptance/bundle/variables/complex-transitive/output.txt create mode 100644 acceptance/bundle/variables/complex-transitive/script rename {bundle/tests => acceptance/bundle}/variables/complex/databricks.yml (98%) create mode 100644 acceptance/bundle/variables/complex/out.default.json create mode 100644 acceptance/bundle/variables/complex/out.dev.json create mode 100644 acceptance/bundle/variables/complex/script rename {bundle/tests => acceptance/bundle}/variables/complex_multiple_files/databricks.yml (100%) create mode 100644 acceptance/bundle/variables/complex_multiple_files/output.txt create mode 100644 acceptance/bundle/variables/complex_multiple_files/script rename {bundle/tests => acceptance/bundle}/variables/complex_multiple_files/variables/clusters.yml (100%) rename {bundle/tests => acceptance/bundle}/variables/empty/databricks.yml (100%) create mode 100644 acceptance/bundle/variables/empty/output.txt create mode 100644 acceptance/bundle/variables/empty/script rename {bundle/tests => acceptance/bundle}/variables/env_overrides/databricks.yml (100%) create mode 100644 acceptance/bundle/variables/env_overrides/output.txt create mode 100644 acceptance/bundle/variables/env_overrides/script rename {bundle/tests => acceptance/bundle}/variables/vanilla/databricks.yml (100%) create mode 100644 acceptance/bundle/variables/vanilla/output.txt create mode 100644 acceptance/bundle/variables/vanilla/script rename {bundle/tests => acceptance/bundle}/variables/variable_overrides_in_target/databricks.yml (100%) create mode 100644 acceptance/bundle/variables/variable_overrides_in_target/output.txt create mode 100644 acceptance/bundle/variables/variable_overrides_in_target/script rename {bundle/tests => acceptance/bundle}/variables/without_definition/databricks.yml (53%) create mode 100644 acceptance/bundle/variables/without_definition/output.txt create mode 100644 acceptance/bundle/variables/without_definition/script create mode 100644 acceptance/help/output.txt create mode 100644 acceptance/help/script create mode 100644 acceptance/script.cleanup create mode 100644 acceptance/script.prepare create mode 100644 acceptance/server_test.go delete mode 100644 bundle/tests/clusters_test.go delete mode 100644 bundle/tests/complex_variables_test.go delete mode 100644 bundle/tests/override_job_cluster_test.go delete mode 100644 bundle/tests/override_job_tasks_test.go delete mode 100644 bundle/tests/override_pipeline_cluster_test.go delete mode 100644 bundle/tests/variables_test.go diff --git a/Makefile b/Makefile index f8b725900..40eef9f31 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ default: build -PACKAGES=./libs/... ./internal/... ./cmd/... ./bundle/... . +PACKAGES=./acceptance/... ./libs/... ./internal/... ./cmd/... ./bundle/... . GOTESTSUM_FORMAT ?= pkgname-and-test-fails diff --git a/acceptance/README.md b/acceptance/README.md new file mode 100644 index 000000000..162c57ea2 --- /dev/null +++ b/acceptance/README.md @@ -0,0 +1,19 @@ +Acceptance tests are blackbox tests that are run against compiled binary. + +Currently these tests are run against "fake" HTTP server pretending to be Databricks API. However, they will be extended to run against real environment as regular integration tests. + +To author a test, + - Add a new directory under `acceptance`. Any level of nesting is supported. + - Add `databricks.yml` there. + - Add `script` with commands to run, e.g. `$CLI bundle validate`. The test case is recognized by presence of `script`. + +The test runner will run script and capture output and compare it with `output.txt` file in the same directory. + +In order to write `output.txt` for the first time or overwrite it with the current output, set `TESTS_OUTPUT=OVERWRITE` env var. + +The scripts are run with `bash -e` so any errors will be propagated. They are captured in `output.txt` by appending `Exit code: N` line at the end. + +For more complex tests one can also use: +- `errcode` helper: if the command fails with non-zero code, it appends `Exit code: N` to the output but returns success to caller (bash), allowing continuation of script. +- `trace` helper: prints the arguments before executing the command. +- custom output files: redirect output to custom file (it must start with `out`), e.g. `$CLI bundle validate > out.txt 2> out.error.txt`. diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go new file mode 100644 index 000000000..759c0aeca --- /dev/null +++ b/acceptance/acceptance_test.go @@ -0,0 +1,302 @@ +package acceptance_test + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "slices" + "sort" + "strings" + "testing" + "time" + + "github.com/databricks/cli/internal/testutil" + "github.com/databricks/cli/libs/env" + "github.com/databricks/cli/libs/testdiff" + "github.com/stretchr/testify/require" +) + +var KeepTmp = os.Getenv("KEEP_TMP") != "" + +const ( + EntryPointScript = "script" + CleanupScript = "script.cleanup" + PrepareScript = "script.prepare" +) + +var Scripts = map[string]bool{ + EntryPointScript: true, + CleanupScript: true, + PrepareScript: true, +} + +func TestAccept(t *testing.T) { + execPath := BuildCLI(t) + // $CLI is what test scripts are using + t.Setenv("CLI", execPath) + + server := StartServer(t) + AddHandlers(server) + // Redirect API access to local server: + t.Setenv("DATABRICKS_HOST", fmt.Sprintf("http://127.0.0.1:%d", server.Port)) + t.Setenv("DATABRICKS_TOKEN", "dapi1234") + + homeDir := t.TempDir() + // Do not read user's ~/.databrickscfg + t.Setenv(env.HomeEnvVar(), homeDir) + + testDirs := getTests(t) + require.NotEmpty(t, testDirs) + for _, dir := range testDirs { + t.Run(dir, func(t *testing.T) { + t.Parallel() + runTest(t, dir) + }) + } +} + +func getTests(t *testing.T) []string { + testDirs := make([]string, 0, 128) + + err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + name := filepath.Base(path) + if name == EntryPointScript { + // Presence of 'script' marks a test case in this directory + testDirs = append(testDirs, filepath.Dir(path)) + } + return nil + }) + require.NoError(t, err) + + sort.Strings(testDirs) + return testDirs +} + +func runTest(t *testing.T, dir string) { + var tmpDir string + var err error + if KeepTmp { + tempDirBase := filepath.Join(os.TempDir(), "acceptance") + _ = os.Mkdir(tempDirBase, 0o755) + tmpDir, err = os.MkdirTemp(tempDirBase, "") + require.NoError(t, err) + t.Logf("Created directory: %s", tmpDir) + } else { + tmpDir = t.TempDir() + } + + scriptContents := readMergedScriptContents(t, dir) + testutil.WriteFile(t, filepath.Join(tmpDir, EntryPointScript), scriptContents) + + inputs := make(map[string]bool, 2) + outputs := make(map[string]bool, 2) + err = CopyDir(dir, tmpDir, inputs, outputs) + require.NoError(t, err) + + args := []string{"bash", "-euo", "pipefail", EntryPointScript} + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = tmpDir + outB, err := cmd.CombinedOutput() + + out := formatOutput(string(outB), err) + out = strings.ReplaceAll(out, os.Getenv("CLI"), "$CLI") + doComparison(t, filepath.Join(dir, "output.txt"), "script output", out) + + for key := range outputs { + if key == "output.txt" { + // handled above + continue + } + pathNew := filepath.Join(tmpDir, key) + newValBytes, err := os.ReadFile(pathNew) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + t.Errorf("%s: expected to find this file but could not (%s)", key, tmpDir) + } else { + t.Errorf("%s: could not read: %s", key, err) + } + continue + } + pathExpected := filepath.Join(dir, key) + doComparison(t, pathExpected, pathNew, string(newValBytes)) + } + + // Make sure there are not unaccounted for new files + files, err := os.ReadDir(tmpDir) + require.NoError(t, err) + + for _, f := range files { + name := f.Name() + if _, ok := inputs[name]; ok { + continue + } + if _, ok := outputs[name]; ok { + continue + } + t.Errorf("Unexpected output: %s", f) + if strings.HasPrefix(name, "out") { + // We have a new file starting with "out" + // Show the contents & support overwrite mode for it: + pathNew := filepath.Join(tmpDir, name) + newVal := testutil.ReadFile(t, pathNew) + doComparison(t, filepath.Join(dir, name), filepath.Join(tmpDir, name), newVal) + } + } +} + +func doComparison(t *testing.T, pathExpected, pathNew, valueNew string) { + valueNew = testdiff.NormalizeNewlines(valueNew) + valueExpected := string(readIfExists(t, pathExpected)) + valueExpected = testdiff.NormalizeNewlines(valueExpected) + testdiff.AssertEqualTexts(t, pathExpected, pathNew, valueExpected, valueNew) + if testdiff.OverwriteMode { + if valueNew != "" { + t.Logf("Overwriting: %s", pathExpected) + testutil.WriteFile(t, pathExpected, valueNew) + } else { + t.Logf("Removing: %s", pathExpected) + _ = os.Remove(pathExpected) + } + } +} + +// Returns combined script.prepare (root) + script.prepare (parent) + ... + script + ... + script.cleanup (parent) + ... +// Note, cleanups are not executed if main script fails; that's not a huge issue, since it runs it temp dir. +func readMergedScriptContents(t *testing.T, dir string) string { + scriptContents := testutil.ReadFile(t, filepath.Join(dir, EntryPointScript)) + prepares := []string{} + cleanups := []string{} + + for { + x := readIfExists(t, filepath.Join(dir, CleanupScript)) + if len(x) > 0 { + cleanups = append(cleanups, string(x)) + } + + x = readIfExists(t, filepath.Join(dir, PrepareScript)) + if len(x) > 0 { + prepares = append(prepares, string(x)) + } + + if dir == "" || dir == "." { + break + } + + dir = filepath.Dir(dir) + require.True(t, filepath.IsLocal(dir)) + } + + slices.Reverse(prepares) + prepares = append(prepares, scriptContents) + prepares = append(prepares, cleanups...) + return strings.Join(prepares, "\n") +} + +func BuildCLI(t *testing.T) string { + cwd, err := os.Getwd() + require.NoError(t, err) + execPath := filepath.Join(cwd, "build", "databricks") + if runtime.GOOS == "windows" { + execPath += ".exe" + } + + start := time.Now() + args := []string{"go", "build", "-mod", "vendor", "-o", execPath} + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = ".." + out, err := cmd.CombinedOutput() + elapsed := time.Since(start) + t.Logf("%s took %s", args, elapsed) + require.NoError(t, err, "go build failed: %s: %s\n%s", args, err, out) + if len(out) > 0 { + t.Logf("go build output: %s: %s", args, out) + } + + // Quick check + warm up cache: + cmd = exec.Command(execPath, "--version") + out, err = cmd.CombinedOutput() + require.NoError(t, err, "%s --version failed: %s\n%s", execPath, err, out) + return execPath +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} + +func formatOutput(out string, err error) string { + if err == nil { + return out + } + if exiterr, ok := err.(*exec.ExitError); ok { + exitCode := exiterr.ExitCode() + out += fmt.Sprintf("\nExit code: %d\n", exitCode) + } else { + out += fmt.Sprintf("\nError: %s\n", err) + } + return out +} + +func readIfExists(t *testing.T, path string) []byte { + data, err := os.ReadFile(path) + if err == nil { + return data + } + + if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("%s: %s", path, err) + } + return []byte{} +} + +func CopyDir(src, dst string, inputs, outputs map[string]bool) error { + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + name := info.Name() + + relPath, err := filepath.Rel(src, path) + if err != nil { + return err + } + + if strings.HasPrefix(name, "out") { + outputs[relPath] = true + return nil + } else { + inputs[relPath] = true + } + + if _, ok := Scripts[name]; ok { + return nil + } + + destPath := filepath.Join(dst, relPath) + + if info.IsDir() { + return os.MkdirAll(destPath, info.Mode()) + } + + return copyFile(path, destPath) + }) +} diff --git a/acceptance/build/.gitignore b/acceptance/build/.gitignore new file mode 100644 index 000000000..a48b4db25 --- /dev/null +++ b/acceptance/build/.gitignore @@ -0,0 +1 @@ +databricks diff --git a/bundle/tests/clusters/databricks.yml b/acceptance/bundle/override/clusters/databricks.yml similarity index 92% rename from bundle/tests/clusters/databricks.yml rename to acceptance/bundle/override/clusters/databricks.yml index 1074462a6..14efceec0 100644 --- a/bundle/tests/clusters/databricks.yml +++ b/acceptance/bundle/override/clusters/databricks.yml @@ -1,9 +1,6 @@ bundle: name: clusters -workspace: - host: https://acme.cloud.databricks.com/ - resources: clusters: foo: diff --git a/acceptance/bundle/override/clusters/output.txt b/acceptance/bundle/override/clusters/output.txt new file mode 100644 index 000000000..cff30b3af --- /dev/null +++ b/acceptance/bundle/override/clusters/output.txt @@ -0,0 +1,33 @@ + +>>> $CLI bundle validate -o json -t default +{ + "autoscale": { + "max_workers": 7, + "min_workers": 2 + }, + "cluster_name": "foo", + "custom_tags": {}, + "node_type_id": "i3.xlarge", + "num_workers": 2, + "spark_conf": { + "spark.executor.memory": "2g" + }, + "spark_version": "13.3.x-scala2.12" +} + +>>> $CLI bundle validate -o json -t development +{ + "autoscale": { + "max_workers": 3, + "min_workers": 1 + }, + "cluster_name": "foo-override", + "custom_tags": {}, + "node_type_id": "m5.xlarge", + "num_workers": 3, + "spark_conf": { + "spark.executor.memory": "4g", + "spark.executor.memory2": "4g" + }, + "spark_version": "15.2.x-scala2.12" +} diff --git a/acceptance/bundle/override/clusters/script b/acceptance/bundle/override/clusters/script new file mode 100644 index 000000000..4a73dd93e --- /dev/null +++ b/acceptance/bundle/override/clusters/script @@ -0,0 +1,2 @@ +trace $CLI bundle validate -o json -t default | jq .resources.clusters.foo +trace $CLI bundle validate -o json -t development | jq .resources.clusters.foo diff --git a/bundle/tests/override_job_cluster/databricks.yml b/acceptance/bundle/override/job_cluster/databricks.yml similarity index 91% rename from bundle/tests/override_job_cluster/databricks.yml rename to acceptance/bundle/override/job_cluster/databricks.yml index a85b3b711..d6b7ede4f 100644 --- a/bundle/tests/override_job_cluster/databricks.yml +++ b/acceptance/bundle/override/job_cluster/databricks.yml @@ -1,9 +1,6 @@ bundle: name: override_job_cluster -workspace: - host: https://acme.cloud.databricks.com/ - resources: jobs: foo: diff --git a/acceptance/bundle/override/job_cluster/output.txt b/acceptance/bundle/override/job_cluster/output.txt new file mode 100644 index 000000000..dc7a5f75b --- /dev/null +++ b/acceptance/bundle/override/job_cluster/output.txt @@ -0,0 +1,30 @@ + +>>> $CLI bundle validate -o json -t development +{ + "name": "job", + "job_clusters": [ + { + "job_cluster_key": "key", + "new_cluster": { + "node_type_id": "i3.xlarge", + "num_workers": 1, + "spark_version": "13.3.x-scala2.12" + } + } + ] +} + +>>> $CLI bundle validate -o json -t staging +{ + "name": "job", + "job_clusters": [ + { + "job_cluster_key": "key", + "new_cluster": { + "node_type_id": "i3.2xlarge", + "num_workers": 4, + "spark_version": "13.3.x-scala2.12" + } + } + ] +} diff --git a/acceptance/bundle/override/job_cluster/script b/acceptance/bundle/override/job_cluster/script new file mode 100644 index 000000000..3f6827bb7 --- /dev/null +++ b/acceptance/bundle/override/job_cluster/script @@ -0,0 +1,2 @@ +trace $CLI bundle validate -o json -t development | jq '.resources.jobs.foo | {name,job_clusters}' +trace $CLI bundle validate -o json -t staging | jq '.resources.jobs.foo | {name,job_clusters}' diff --git a/bundle/tests/override_job_tasks/databricks.yml b/acceptance/bundle/override/job_tasks/databricks.yml similarity index 94% rename from bundle/tests/override_job_tasks/databricks.yml rename to acceptance/bundle/override/job_tasks/databricks.yml index ddee28793..fd7edafb9 100644 --- a/bundle/tests/override_job_tasks/databricks.yml +++ b/acceptance/bundle/override/job_tasks/databricks.yml @@ -1,9 +1,6 @@ bundle: name: override_job_tasks -workspace: - host: https://acme.cloud.databricks.com/ - resources: jobs: foo: diff --git a/acceptance/bundle/override/job_tasks/output.txt b/acceptance/bundle/override/job_tasks/output.txt new file mode 100644 index 000000000..0d561291e --- /dev/null +++ b/acceptance/bundle/override/job_tasks/output.txt @@ -0,0 +1,68 @@ + +>>> errcode $CLI bundle validate -o json -t development +Error: file ./test1.py not found + +Exit code: 1 +{ + "name": "job", + "queue": { + "enabled": true + }, + "tags": {}, + "tasks": [ + { + "new_cluster": { + "node_type_id": "i3.xlarge", + "num_workers": 1, + "spark_version": "13.3.x-scala2.12" + }, + "spark_python_task": { + "python_file": "./test1.py" + }, + "task_key": "key1" + }, + { + "new_cluster": { + "spark_version": "13.3.x-scala2.12" + }, + "spark_python_task": { + "python_file": "./test2.py" + }, + "task_key": "key2" + } + ] +} + +>>> errcode $CLI bundle validate -o json -t staging +Error: file ./test1.py not found + +Exit code: 1 +{ + "name": "job", + "queue": { + "enabled": true + }, + "tags": {}, + "tasks": [ + { + "new_cluster": { + "spark_version": "13.3.x-scala2.12" + }, + "spark_python_task": { + "python_file": "./test1.py" + }, + "task_key": "key1" + }, + { + "new_cluster": { + "node_type_id": "i3.2xlarge", + "num_workers": 4, + "spark_version": "13.3.x-scala2.12" + }, + "spark_python_task": { + "python_file": "./test3.py" + }, + "task_key": "key2" + } + ] +} diff --git a/acceptance/bundle/override/job_tasks/script b/acceptance/bundle/override/job_tasks/script new file mode 100644 index 000000000..4e0869857 --- /dev/null +++ b/acceptance/bundle/override/job_tasks/script @@ -0,0 +1,2 @@ +trace errcode $CLI bundle validate -o json -t development | jq .resources.jobs.foo +trace errcode $CLI bundle validate -o json -t staging | jq .resources.jobs.foo diff --git a/acceptance/bundle/override/merge-string-map/databricks.yml b/acceptance/bundle/override/merge-string-map/databricks.yml new file mode 100644 index 000000000..5e443ceca --- /dev/null +++ b/acceptance/bundle/override/merge-string-map/databricks.yml @@ -0,0 +1,13 @@ +bundle: + name: merge-string-map + +resources: + clusters: + my_cluster: "hello" + +targets: + dev: + resources: + clusters: + my_cluster: + spark_version: "25" diff --git a/acceptance/bundle/override/merge-string-map/output.txt b/acceptance/bundle/override/merge-string-map/output.txt new file mode 100644 index 000000000..e1bd7dfb4 --- /dev/null +++ b/acceptance/bundle/override/merge-string-map/output.txt @@ -0,0 +1,23 @@ + +>>> $CLI bundle validate -o json -t dev +{ + "clusters": { + "my_cluster": { + "custom_tags": {}, + "spark_version": "25" + } + } +} + +>>> $CLI bundle validate -t dev +Warning: expected map, found string + at resources.clusters.my_cluster + in databricks.yml:6:17 + +Name: merge-string-map +Target: dev +Workspace: + User: tester@databricks.com + Path: /Workspace/Users/tester@databricks.com/.bundle/merge-string-map/dev + +Found 1 warning diff --git a/acceptance/bundle/override/merge-string-map/script b/acceptance/bundle/override/merge-string-map/script new file mode 100644 index 000000000..a109d5f69 --- /dev/null +++ b/acceptance/bundle/override/merge-string-map/script @@ -0,0 +1,2 @@ +trace $CLI bundle validate -o json -t dev | jq .resources +trace $CLI bundle validate -t dev diff --git a/bundle/tests/override_pipeline_cluster/databricks.yml b/acceptance/bundle/override/pipeline_cluster/databricks.yml similarity index 90% rename from bundle/tests/override_pipeline_cluster/databricks.yml rename to acceptance/bundle/override/pipeline_cluster/databricks.yml index 8930f30e8..8b4857460 100644 --- a/bundle/tests/override_pipeline_cluster/databricks.yml +++ b/acceptance/bundle/override/pipeline_cluster/databricks.yml @@ -1,9 +1,6 @@ bundle: name: override_pipeline_cluster -workspace: - host: https://acme.cloud.databricks.com/ - resources: pipelines: foo: diff --git a/acceptance/bundle/override/pipeline_cluster/output.txt b/acceptance/bundle/override/pipeline_cluster/output.txt new file mode 100644 index 000000000..81bf58180 --- /dev/null +++ b/acceptance/bundle/override/pipeline_cluster/output.txt @@ -0,0 +1,44 @@ + +>>> $CLI bundle validate -o json -t development +{ + "foo": { + "clusters": [ + { + "label": "default", + "node_type_id": "i3.xlarge", + "num_workers": 1, + "spark_conf": { + "foo": "bar" + } + } + ], + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/override_pipeline_cluster/development/state/metadata.json" + }, + "name": "job", + "permissions": [] + } +} + +>>> $CLI bundle validate -o json -t staging +{ + "foo": { + "clusters": [ + { + "label": "default", + "node_type_id": "i3.2xlarge", + "num_workers": 4, + "spark_conf": { + "foo": "bar" + } + } + ], + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/override_pipeline_cluster/staging/state/metadata.json" + }, + "name": "job", + "permissions": [] + } +} diff --git a/acceptance/bundle/override/pipeline_cluster/script b/acceptance/bundle/override/pipeline_cluster/script new file mode 100644 index 000000000..b06005ce5 --- /dev/null +++ b/acceptance/bundle/override/pipeline_cluster/script @@ -0,0 +1,2 @@ +trace $CLI bundle validate -o json -t development | jq .resources.pipelines +trace $CLI bundle validate -o json -t staging | jq .resources.pipelines diff --git a/acceptance/bundle/variables/complex-transitive/databricks.yml b/acceptance/bundle/variables/complex-transitive/databricks.yml new file mode 100644 index 000000000..9ef4e6386 --- /dev/null +++ b/acceptance/bundle/variables/complex-transitive/databricks.yml @@ -0,0 +1,19 @@ +bundle: + name: complex-transitive + +variables: + catalog: + default: hive_metastore + spark_conf: + default: + "spark.databricks.sql.initial.catalog.name": ${var.catalog} + etl_cluster_config: + type: complex + default: + spark_version: 14.3.x-scala2.12 + runtime_engine: PHOTON + spark_conf: ${var.spark_conf} + +resources: + clusters: + my_cluster: ${var.etl_cluster_config} diff --git a/acceptance/bundle/variables/complex-transitive/output.txt b/acceptance/bundle/variables/complex-transitive/output.txt new file mode 100644 index 000000000..a031e0497 --- /dev/null +++ b/acceptance/bundle/variables/complex-transitive/output.txt @@ -0,0 +1,3 @@ +{ + "spark.databricks.sql.initial.catalog.name": "${var.catalog}" +} diff --git a/acceptance/bundle/variables/complex-transitive/script b/acceptance/bundle/variables/complex-transitive/script new file mode 100644 index 000000000..52bb08ed4 --- /dev/null +++ b/acceptance/bundle/variables/complex-transitive/script @@ -0,0 +1,2 @@ +# Currently, this incorrectly outputs variable reference instead of resolved value +$CLI bundle validate -o json | jq '.resources.clusters.my_cluster.spark_conf' diff --git a/bundle/tests/variables/complex/databricks.yml b/acceptance/bundle/variables/complex/databricks.yml similarity index 98% rename from bundle/tests/variables/complex/databricks.yml rename to acceptance/bundle/variables/complex/databricks.yml index 3b32a7c8e..500f374e3 100644 --- a/bundle/tests/variables/complex/databricks.yml +++ b/acceptance/bundle/variables/complex/databricks.yml @@ -46,6 +46,7 @@ variables: targets: default: + default: true dev: variables: node_type: "Standard_DS3_v3" diff --git a/acceptance/bundle/variables/complex/out.default.json b/acceptance/bundle/variables/complex/out.default.json new file mode 100644 index 000000000..bbdb0f8de --- /dev/null +++ b/acceptance/bundle/variables/complex/out.default.json @@ -0,0 +1,124 @@ +{ + "resources": { + "jobs": { + "my_job": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/complex-variables/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "job_clusters": [ + { + "job_cluster_key": "key", + "new_cluster": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 2, + "policy_id": "some-policy-id", + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": "false", + "spark.random": "true", + "spark.speculation": "true" + }, + "spark_version": "13.2.x-scala2.11" + } + } + ], + "permissions": [], + "queue": { + "enabled": true + }, + "tags": {}, + "tasks": [ + { + "job_cluster_key": "key", + "libraries": [ + { + "jar": "/path/to/jar" + }, + { + "egg": "/path/to/egg" + }, + { + "whl": "/path/to/whl" + } + ], + "task_key": "task with spark version 13.2.x-scala2.11 and jar /path/to/jar" + } + ] + } + } + }, + "variables": { + "cluster": { + "default": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 2, + "policy_id": "some-policy-id", + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.random": true, + "spark.speculation": true + }, + "spark_version": "13.2.x-scala2.11" + }, + "description": "A cluster definition", + "type": "complex", + "value": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 2, + "policy_id": "some-policy-id", + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.random": true, + "spark.speculation": true + }, + "spark_version": "13.2.x-scala2.11" + } + }, + "complexvar": { + "default": { + "key1": "value1", + "key2": "value2", + "key3": "value3" + }, + "description": "A complex variable", + "type": "complex", + "value": { + "key1": "value1", + "key2": "value2", + "key3": "value3" + } + }, + "libraries": { + "default": [ + { + "jar": "/path/to/jar" + }, + { + "egg": "/path/to/egg" + }, + { + "whl": "/path/to/whl" + } + ], + "description": "A libraries definition", + "type": "complex", + "value": [ + { + "jar": "/path/to/jar" + }, + { + "egg": "/path/to/egg" + }, + { + "whl": "/path/to/whl" + } + ] + }, + "node_type": { + "default": "Standard_DS3_v2", + "value": "Standard_DS3_v2" + } + } +} diff --git a/acceptance/bundle/variables/complex/out.dev.json b/acceptance/bundle/variables/complex/out.dev.json new file mode 100644 index 000000000..330518c85 --- /dev/null +++ b/acceptance/bundle/variables/complex/out.dev.json @@ -0,0 +1,118 @@ +{ + "resources": { + "jobs": { + "my_job": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/complex-variables/dev/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "job_clusters": [ + { + "job_cluster_key": "key", + "new_cluster": { + "node_type_id": "Standard_DS3_v3", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": "false", + "spark.speculation": "false" + }, + "spark_version": "14.2.x-scala2.11" + } + } + ], + "permissions": [], + "queue": { + "enabled": true + }, + "tags": {}, + "tasks": [ + { + "job_cluster_key": "key", + "libraries": [ + { + "jar": "/path/to/jar" + }, + { + "egg": "/path/to/egg" + }, + { + "whl": "/path/to/whl" + } + ], + "task_key": "task with spark version 14.2.x-scala2.11 and jar /path/to/jar" + } + ] + } + } + }, + "variables": { + "cluster": { + "default": { + "node_type_id": "Standard_DS3_v3", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + }, + "description": "A cluster definition", + "type": "complex", + "value": { + "node_type_id": "Standard_DS3_v3", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + } + }, + "complexvar": { + "default": { + "key1": "1", + "key2": "2", + "key3": "3" + }, + "description": "A complex variable", + "type": "complex", + "value": { + "key1": "1", + "key2": "2", + "key3": "3" + } + }, + "libraries": { + "default": [ + { + "jar": "/path/to/jar" + }, + { + "egg": "/path/to/egg" + }, + { + "whl": "/path/to/whl" + } + ], + "description": "A libraries definition", + "type": "complex", + "value": [ + { + "jar": "/path/to/jar" + }, + { + "egg": "/path/to/egg" + }, + { + "whl": "/path/to/whl" + } + ] + }, + "node_type": { + "default": "Standard_DS3_v3", + "value": "Standard_DS3_v3" + } + } +} diff --git a/acceptance/bundle/variables/complex/script b/acceptance/bundle/variables/complex/script new file mode 100644 index 000000000..9ee0ab02a --- /dev/null +++ b/acceptance/bundle/variables/complex/script @@ -0,0 +1,4 @@ +$CLI bundle validate -o json | jq '{resources,variables}' > out.default.json + +# spark.random and policy_id should be empty in this target: +$CLI bundle validate -o json -t dev | jq '{resources,variables}' > out.dev.json diff --git a/bundle/tests/variables/complex_multiple_files/databricks.yml b/acceptance/bundle/variables/complex_multiple_files/databricks.yml similarity index 100% rename from bundle/tests/variables/complex_multiple_files/databricks.yml rename to acceptance/bundle/variables/complex_multiple_files/databricks.yml diff --git a/acceptance/bundle/variables/complex_multiple_files/output.txt b/acceptance/bundle/variables/complex_multiple_files/output.txt new file mode 100644 index 000000000..e87b8df11 --- /dev/null +++ b/acceptance/bundle/variables/complex_multiple_files/output.txt @@ -0,0 +1,159 @@ +{ + "resources": { + "jobs": { + "my_job": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/complex-variables-multiple-files/dev/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "job_clusters": [ + { + "job_cluster_key": "key1", + "new_cluster": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": "false", + "spark.speculation": "false" + }, + "spark_version": "14.2.x-scala2.11" + } + }, + { + "job_cluster_key": "key2", + "new_cluster": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": "false", + "spark.speculation": "false" + }, + "spark_version": "14.2.x-scala2.11" + } + }, + { + "job_cluster_key": "key3", + "new_cluster": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": "false", + "spark.speculation": "false" + }, + "spark_version": "14.2.x-scala2.11" + } + }, + { + "job_cluster_key": "key4", + "new_cluster": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": "false", + "spark.speculation": "false" + }, + "spark_version": "14.2.x-scala2.11" + } + } + ], + "permissions": [], + "queue": { + "enabled": true + }, + "tags": {} + } + } + }, + "variables": { + "cluster1": { + "default": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + }, + "description": "A cluster definition", + "type": "complex", + "value": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + } + }, + "cluster2": { + "default": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + }, + "description": "A cluster definition", + "type": "complex", + "value": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + } + }, + "cluster3": { + "default": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + }, + "description": "A cluster definition", + "type": "complex", + "value": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + } + }, + "cluster4": { + "default": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + }, + "description": "A cluster definition", + "type": "complex", + "value": { + "node_type_id": "Standard_DS3_v2", + "num_workers": 4, + "spark_conf": { + "spark.databricks.delta.retentionDurationCheck.enabled": false, + "spark.speculation": false + }, + "spark_version": "14.2.x-scala2.11" + } + } + } +} diff --git a/acceptance/bundle/variables/complex_multiple_files/script b/acceptance/bundle/variables/complex_multiple_files/script new file mode 100644 index 000000000..24f1d58d5 --- /dev/null +++ b/acceptance/bundle/variables/complex_multiple_files/script @@ -0,0 +1 @@ +$CLI bundle validate -t dev -o json | jq '{resources, variables}' diff --git a/bundle/tests/variables/complex_multiple_files/variables/clusters.yml b/acceptance/bundle/variables/complex_multiple_files/variables/clusters.yml similarity index 100% rename from bundle/tests/variables/complex_multiple_files/variables/clusters.yml rename to acceptance/bundle/variables/complex_multiple_files/variables/clusters.yml diff --git a/bundle/tests/variables/empty/databricks.yml b/acceptance/bundle/variables/empty/databricks.yml similarity index 100% rename from bundle/tests/variables/empty/databricks.yml rename to acceptance/bundle/variables/empty/databricks.yml diff --git a/acceptance/bundle/variables/empty/output.txt b/acceptance/bundle/variables/empty/output.txt new file mode 100644 index 000000000..c3f0af130 --- /dev/null +++ b/acceptance/bundle/variables/empty/output.txt @@ -0,0 +1,11 @@ +Error: no value assigned to required variable a. Assignment can be done through the "--var" flag or by setting the BUNDLE_VAR_a environment variable + +Name: empty${var.a} +Target: default +Workspace: + User: tester@databricks.com + Path: /Workspace/Users/tester@databricks.com/.bundle/empty${var.a}/default + +Found 1 error + +Exit code: 1 diff --git a/acceptance/bundle/variables/empty/script b/acceptance/bundle/variables/empty/script new file mode 100644 index 000000000..72555b332 --- /dev/null +++ b/acceptance/bundle/variables/empty/script @@ -0,0 +1 @@ +$CLI bundle validate diff --git a/bundle/tests/variables/env_overrides/databricks.yml b/acceptance/bundle/variables/env_overrides/databricks.yml similarity index 100% rename from bundle/tests/variables/env_overrides/databricks.yml rename to acceptance/bundle/variables/env_overrides/databricks.yml diff --git a/acceptance/bundle/variables/env_overrides/output.txt b/acceptance/bundle/variables/env_overrides/output.txt new file mode 100644 index 000000000..e8fb99938 --- /dev/null +++ b/acceptance/bundle/variables/env_overrides/output.txt @@ -0,0 +1,40 @@ + +>>> $CLI bundle validate -t env-with-single-variable-override -o json +"default-a dev-b" + +>>> $CLI bundle validate -t env-with-two-variable-overrides -o json +"prod-a prod-b" + +>>> BUNDLE_VAR_b=env-var-b $CLI bundle validate -t env-with-two-variable-overrides -o json +"prod-a env-var-b" + +>>> errcode $CLI bundle validate -t env-missing-a-required-variable-assignment +Error: no value assigned to required variable b. Assignment can be done through the "--var" flag or by setting the BUNDLE_VAR_b environment variable + +Name: test bundle +Target: env-missing-a-required-variable-assignment +Workspace: + User: tester@databricks.com + Path: /Workspace/Users/tester@databricks.com/.bundle/test bundle/env-missing-a-required-variable-assignment + +Found 1 error + +Exit code: 1 + +>>> errcode $CLI bundle validate -t env-using-an-undefined-variable +Error: variable c is not defined but is assigned a value + +Name: test bundle + +Found 1 error + +Exit code: 1 + +>>> $CLI bundle validate -t env-overrides-lookup -o json +{ + "a": "default-a", + "b": "prod-b", + "d": "4321", + "e": "1234", + "f": "9876" +} diff --git a/acceptance/bundle/variables/env_overrides/script b/acceptance/bundle/variables/env_overrides/script new file mode 100644 index 000000000..30919fd8a --- /dev/null +++ b/acceptance/bundle/variables/env_overrides/script @@ -0,0 +1,6 @@ +trace $CLI bundle validate -t env-with-single-variable-override -o json | jq .workspace.profile +trace $CLI bundle validate -t env-with-two-variable-overrides -o json | jq .workspace.profile +trace BUNDLE_VAR_b=env-var-b $CLI bundle validate -t env-with-two-variable-overrides -o json | jq .workspace.profile +trace errcode $CLI bundle validate -t env-missing-a-required-variable-assignment +trace errcode $CLI bundle validate -t env-using-an-undefined-variable +trace $CLI bundle validate -t env-overrides-lookup -o json | jq '.variables | map_values(.value)' diff --git a/bundle/tests/variables/vanilla/databricks.yml b/acceptance/bundle/variables/vanilla/databricks.yml similarity index 100% rename from bundle/tests/variables/vanilla/databricks.yml rename to acceptance/bundle/variables/vanilla/databricks.yml diff --git a/acceptance/bundle/variables/vanilla/output.txt b/acceptance/bundle/variables/vanilla/output.txt new file mode 100644 index 000000000..69b358a3f --- /dev/null +++ b/acceptance/bundle/variables/vanilla/output.txt @@ -0,0 +1,16 @@ + +>>> BUNDLE_VAR_b=def $CLI bundle validate -o json +"abc def" + +>>> errcode $CLI bundle validate +Error: no value assigned to required variable b. Assignment can be done through the "--var" flag or by setting the BUNDLE_VAR_b environment variable + +Name: ${var.a} ${var.b} +Target: default +Workspace: + User: tester@databricks.com + Path: /Workspace/Users/tester@databricks.com/.bundle/${var.a} ${var.b}/default + +Found 1 error + +Exit code: 1 diff --git a/acceptance/bundle/variables/vanilla/script b/acceptance/bundle/variables/vanilla/script new file mode 100644 index 000000000..10da3183d --- /dev/null +++ b/acceptance/bundle/variables/vanilla/script @@ -0,0 +1,2 @@ +trace BUNDLE_VAR_b=def $CLI bundle validate -o json | jq .bundle.name +trace errcode $CLI bundle validate diff --git a/bundle/tests/variables/variable_overrides_in_target/databricks.yml b/acceptance/bundle/variables/variable_overrides_in_target/databricks.yml similarity index 100% rename from bundle/tests/variables/variable_overrides_in_target/databricks.yml rename to acceptance/bundle/variables/variable_overrides_in_target/databricks.yml diff --git a/acceptance/bundle/variables/variable_overrides_in_target/output.txt b/acceptance/bundle/variables/variable_overrides_in_target/output.txt new file mode 100644 index 000000000..de193f5b6 --- /dev/null +++ b/acceptance/bundle/variables/variable_overrides_in_target/output.txt @@ -0,0 +1,84 @@ + +>>> $CLI bundle validate -o json -t use-default-variable-values +{ + "pipelines": { + "my_pipeline": { + "clusters": [ + { + "label": "default", + "num_workers": 42 + } + ], + "continuous": true, + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/foobar/use-default-variable-values/state/metadata.json" + }, + "name": "a_string", + "permissions": [] + } + } +} + +>>> $CLI bundle validate -o json -t override-string-variable +{ + "pipelines": { + "my_pipeline": { + "clusters": [ + { + "label": "default", + "num_workers": 42 + } + ], + "continuous": true, + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/foobar/override-string-variable/state/metadata.json" + }, + "name": "overridden_string", + "permissions": [] + } + } +} + +>>> $CLI bundle validate -o json -t override-int-variable +{ + "pipelines": { + "my_pipeline": { + "clusters": [ + { + "label": "default", + "num_workers": 43 + } + ], + "continuous": true, + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/foobar/override-int-variable/state/metadata.json" + }, + "name": "a_string", + "permissions": [] + } + } +} + +>>> $CLI bundle validate -o json -t override-both-bool-and-string-variables +{ + "pipelines": { + "my_pipeline": { + "clusters": [ + { + "label": "default", + "num_workers": 42 + } + ], + "continuous": false, + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/foobar/override-both-bool-and-string-variables/state/metadata.json" + }, + "name": "overridden_string", + "permissions": [] + } + } +} diff --git a/acceptance/bundle/variables/variable_overrides_in_target/script b/acceptance/bundle/variables/variable_overrides_in_target/script new file mode 100644 index 000000000..686b3102a --- /dev/null +++ b/acceptance/bundle/variables/variable_overrides_in_target/script @@ -0,0 +1,4 @@ +trace $CLI bundle validate -o json -t use-default-variable-values | jq .resources +trace $CLI bundle validate -o json -t override-string-variable | jq .resources +trace $CLI bundle validate -o json -t override-int-variable | jq .resources +trace $CLI bundle validate -o json -t override-both-bool-and-string-variables | jq .resources diff --git a/bundle/tests/variables/without_definition/databricks.yml b/acceptance/bundle/variables/without_definition/databricks.yml similarity index 53% rename from bundle/tests/variables/without_definition/databricks.yml rename to acceptance/bundle/variables/without_definition/databricks.yml index 68227b683..c26a85f56 100644 --- a/bundle/tests/variables/without_definition/databricks.yml +++ b/acceptance/bundle/variables/without_definition/databricks.yml @@ -1,3 +1,5 @@ +bundle: + name: x variables: a: b: diff --git a/acceptance/bundle/variables/without_definition/output.txt b/acceptance/bundle/variables/without_definition/output.txt new file mode 100644 index 000000000..4dd1e6609 --- /dev/null +++ b/acceptance/bundle/variables/without_definition/output.txt @@ -0,0 +1,4 @@ +{ + "a": "foo", + "b": "bar" +} diff --git a/acceptance/bundle/variables/without_definition/script b/acceptance/bundle/variables/without_definition/script new file mode 100644 index 000000000..49b9b5448 --- /dev/null +++ b/acceptance/bundle/variables/without_definition/script @@ -0,0 +1 @@ +BUNDLE_VAR_a=foo BUNDLE_VAR_b=bar $CLI bundle validate -o json | jq '.variables | map_values(.value)' diff --git a/acceptance/help/output.txt b/acceptance/help/output.txt new file mode 100644 index 000000000..ed4a88ce6 --- /dev/null +++ b/acceptance/help/output.txt @@ -0,0 +1,143 @@ +Databricks CLI + +Usage: + databricks [command] + +Databricks Workspace + fs Filesystem related commands + git-credentials Registers personal access token for Databricks to do operations on behalf of the user. + repos The Repos API allows users to manage their git repos. + secrets The Secrets API allows you to manage secrets, secret scopes, and access permissions. + workspace The Workspace API allows you to list, import, export, and delete notebooks and folders. + +Compute + cluster-policies You can use cluster policies to control users' ability to configure clusters based on a set of rules. + clusters The Clusters API allows you to create, start, edit, list, terminate, and delete clusters. + global-init-scripts The Global Init Scripts API enables Workspace administrators to configure global initialization scripts for their workspace. + instance-pools Instance Pools API are used to create, edit, delete and list instance pools by using ready-to-use cloud instances which reduces a cluster start and auto-scaling times. + instance-profiles The Instance Profiles API allows admins to add, list, and remove instance profiles that users can launch clusters with. + libraries The Libraries API allows you to install and uninstall libraries and get the status of libraries on a cluster. + policy-compliance-for-clusters The policy compliance APIs allow you to view and manage the policy compliance status of clusters in your workspace. + policy-families View available policy families. + +Workflows + jobs The Jobs API allows you to create, edit, and delete jobs. + policy-compliance-for-jobs The compliance APIs allow you to view and manage the policy compliance status of jobs in your workspace. + +Delta Live Tables + pipelines The Delta Live Tables API allows you to create, edit, delete, start, and view details about pipelines. + +Machine Learning + experiments Experiments are the primary unit of organization in MLflow; all MLflow runs belong to an experiment. + model-registry Note: This API reference documents APIs for the Workspace Model Registry. + +Real-time Serving + serving-endpoints The Serving Endpoints API allows you to create, update, and delete model serving endpoints. + +Identity and Access Management + current-user This API allows retrieving information about currently authenticated user or service principal. + groups Groups simplify identity management, making it easier to assign access to Databricks workspace, data, and other securable objects. + permissions Permissions API are used to create read, write, edit, update and manage access for various users on different objects and endpoints. + service-principals Identities for use with jobs, automated tools, and systems such as scripts, apps, and CI/CD platforms. + users User identities recognized by Databricks and represented by email addresses. + +Databricks SQL + alerts The alerts API can be used to perform CRUD operations on alerts. + alerts-legacy The alerts API can be used to perform CRUD operations on alerts. + dashboards In general, there is little need to modify dashboards using the API. + data-sources This API is provided to assist you in making new query objects. + queries The queries API can be used to perform CRUD operations on queries. + queries-legacy These endpoints are used for CRUD operations on query definitions. + query-history A service responsible for storing and retrieving the list of queries run against SQL endpoints and serverless compute. + warehouses A SQL warehouse is a compute resource that lets you run SQL commands on data objects within Databricks SQL. + +Unity Catalog + artifact-allowlists In Databricks Runtime 13.3 and above, you can add libraries and init scripts to the allowlist in UC so that users can leverage these artifacts on compute configured with shared access mode. + catalogs A catalog is the first layer of Unity Catalog’s three-level namespace. + connections Connections allow for creating a connection to an external data source. + credentials A credential represents an authentication and authorization mechanism for accessing services on your cloud tenant. + external-locations An external location is an object that combines a cloud storage path with a storage credential that authorizes access to the cloud storage path. + functions Functions implement User-Defined Functions (UDFs) in Unity Catalog. + grants In Unity Catalog, data is secure by default. + metastores A metastore is the top-level container of objects in Unity Catalog. + model-versions Databricks provides a hosted version of MLflow Model Registry in Unity Catalog. + online-tables Online tables provide lower latency and higher QPS access to data from Delta tables. + quality-monitors A monitor computes and monitors data or model quality metrics for a table over time. + registered-models Databricks provides a hosted version of MLflow Model Registry in Unity Catalog. + resource-quotas Unity Catalog enforces resource quotas on all securable objects, which limits the number of resources that can be created. + schemas A schema (also called a database) is the second layer of Unity Catalog’s three-level namespace. + storage-credentials A storage credential represents an authentication and authorization mechanism for accessing data stored on your cloud tenant. + system-schemas A system schema is a schema that lives within the system catalog. + table-constraints Primary key and foreign key constraints encode relationships between fields in tables. + tables A table resides in the third layer of Unity Catalog’s three-level namespace. + temporary-table-credentials Temporary Table Credentials refer to short-lived, downscoped credentials used to access cloud storage locationswhere table data is stored in Databricks. + volumes Volumes are a Unity Catalog (UC) capability for accessing, storing, governing, organizing and processing files. + workspace-bindings A securable in Databricks can be configured as __OPEN__ or __ISOLATED__. + +Delta Sharing + providers A data provider is an object representing the organization in the real world who shares the data. + recipient-activation The Recipient Activation API is only applicable in the open sharing model where the recipient object has the authentication type of TOKEN. + recipients A recipient is an object you create using :method:recipients/create to represent an organization which you want to allow access shares. + shares A share is a container instantiated with :method:shares/create. + +Settings + ip-access-lists IP Access List enables admins to configure IP access lists. + notification-destinations The notification destinations API lets you programmatically manage a workspace's notification destinations. + settings Workspace Settings API allows users to manage settings at the workspace level. + token-management Enables administrators to get all tokens and delete tokens for other users. + tokens The Token API allows you to create, list, and revoke tokens that can be used to authenticate and access Databricks REST APIs. + workspace-conf This API allows updating known workspace settings for advanced users. + +Developer Tools + bundle Databricks Asset Bundles let you express data/AI/analytics projects as code. + sync Synchronize a local directory to a workspace directory + +Vector Search + vector-search-endpoints **Endpoint**: Represents the compute resources to host vector search indexes. + vector-search-indexes **Index**: An efficient representation of your embedding vectors that supports real-time and efficient approximate nearest neighbor (ANN) search queries. + +Dashboards + lakeview These APIs provide specific management operations for Lakeview dashboards. + +Marketplace + consumer-fulfillments Fulfillments are entities that allow consumers to preview installations. + consumer-installations Installations are entities that allow consumers to interact with Databricks Marketplace listings. + consumer-listings Listings are the core entities in the Marketplace. + consumer-personalization-requests Personalization Requests allow customers to interact with the individualized Marketplace listing flow. + consumer-providers Providers are the entities that publish listings to the Marketplace. + provider-exchange-filters Marketplace exchanges filters curate which groups can access an exchange. + provider-exchanges Marketplace exchanges allow providers to share their listings with a curated set of customers. + provider-files Marketplace offers a set of file APIs for various purposes such as preview notebooks and provider icons. + provider-listings Listings are the core entities in the Marketplace. + provider-personalization-requests Personalization requests are an alternate to instantly available listings. + provider-provider-analytics-dashboards Manage templated analytics solution for providers. + provider-providers Providers are entities that manage assets in Marketplace. + +Apps + apps Apps run directly on a customer’s Databricks instance, integrate with their data, use and extend Databricks services, and enable users to interact through single sign-on. + apps Apps run directly on a customer’s Databricks instance, integrate with their data, use and extend Databricks services, and enable users to interact through single sign-on. + +Clean Rooms + clean-room-assets Clean room assets are data and code objects — Tables, volumes, and notebooks that are shared with the clean room. + clean-room-task-runs Clean room task runs are the executions of notebooks in a clean room. + clean-rooms A clean room uses Delta Sharing and serverless compute to provide a secure and privacy-protecting environment where multiple parties can work together on sensitive enterprise data without direct access to each other’s data. + +Additional Commands: + account Databricks Account Commands + api Perform Databricks API call + auth Authentication related commands + completion Generate the autocompletion script for the specified shell + configure Configure authentication + help Help about any command + labs Manage Databricks Labs installations + version Retrieve information about the current version of this CLI + +Flags: + --debug enable debug logging + -h, --help help for databricks + -o, --output type output type: text or json (default text) + -p, --profile string ~/.databrickscfg profile + -t, --target string bundle target to use (if applicable) + -v, --version version for databricks + +Use "databricks [command] --help" for more information about a command. diff --git a/acceptance/help/script b/acceptance/help/script new file mode 100644 index 000000000..5fa569470 --- /dev/null +++ b/acceptance/help/script @@ -0,0 +1 @@ +$CLI diff --git a/acceptance/script.cleanup b/acceptance/script.cleanup new file mode 100644 index 000000000..3c3e29ebc --- /dev/null +++ b/acceptance/script.cleanup @@ -0,0 +1 @@ +rm -fr .databricks .gitignore diff --git a/acceptance/script.prepare b/acceptance/script.prepare new file mode 100644 index 000000000..3f1bb2acc --- /dev/null +++ b/acceptance/script.prepare @@ -0,0 +1,36 @@ +# Prevent CLI from downloading terraform in each test: +export DATABRICKS_TF_EXEC_PATH=/tmp/ + +errcode() { + # Temporarily disable 'set -e' to prevent the script from exiting on error + set +e + # Execute the provided command with all arguments + "$@" + local exit_code=$? + # Re-enable 'set -e' if it was previously set + set -e + >&2 printf "\nExit code: $exit_code\n" +} + +trace() { + >&2 printf "\n>>> %s\n" "$*" + + if [[ "$1" == *"="* ]]; then + # If the first argument contains '=', collect all env vars + local env_vars=() + while [[ "$1" == *"="* ]]; do + env_vars+=("$1") + shift + done + # Export environment variables in a subshell and execute the command + ( + export "${env_vars[@]}" + "$@" + ) + else + # Execute the command normally + "$@" + fi + + return $? +} diff --git a/acceptance/server_test.go b/acceptance/server_test.go new file mode 100644 index 000000000..7b21e198f --- /dev/null +++ b/acceptance/server_test.go @@ -0,0 +1,129 @@ +package acceptance_test + +import ( + "encoding/json" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/databricks/databricks-sdk-go/service/compute" + "github.com/databricks/databricks-sdk-go/service/iam" + "github.com/databricks/databricks-sdk-go/service/workspace" +) + +type TestServer struct { + *httptest.Server + Mux *http.ServeMux + Port int +} + +type HandlerFunc func(r *http.Request) (any, error) + +func NewTestServer() *TestServer { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + port := server.Listener.Addr().(*net.TCPAddr).Port + + return &TestServer{ + Server: server, + Mux: mux, + Port: port, + } +} + +func (s *TestServer) Handle(pattern string, handler HandlerFunc) { + s.Mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { + resp, err := handler(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + + var respBytes []byte + + respString, ok := resp.(string) + if ok { + respBytes = []byte(respString) + } else { + respBytes, err = json.MarshalIndent(resp, "", " ") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + if _, err := w.Write(respBytes); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) +} + +func StartServer(t *testing.T) *TestServer { + server := NewTestServer() + t.Cleanup(func() { + server.Close() + }) + return server +} + +func AddHandlers(server *TestServer) { + server.Handle("/api/2.0/policies/clusters/list", func(r *http.Request) (any, error) { + return compute.ListPoliciesResponse{ + Policies: []compute.Policy{ + { + PolicyId: "5678", + Name: "wrong-cluster-policy", + }, + { + PolicyId: "9876", + Name: "some-test-cluster-policy", + }, + }, + }, nil + }) + + server.Handle("/api/2.0/instance-pools/list", func(r *http.Request) (any, error) { + return compute.ListInstancePools{ + InstancePools: []compute.InstancePoolAndStats{ + { + InstancePoolName: "some-test-instance-pool", + InstancePoolId: "1234", + }, + }, + }, nil + }) + + server.Handle("/api/2.1/clusters/list", func(r *http.Request) (any, error) { + return compute.ListClustersResponse{ + Clusters: []compute.ClusterDetails{ + { + ClusterName: "some-test-cluster", + ClusterId: "4321", + }, + { + ClusterName: "some-other-cluster", + ClusterId: "9876", + }, + }, + }, nil + }) + + server.Handle("/api/2.0/preview/scim/v2/Me", func(r *http.Request) (any, error) { + return iam.User{ + UserName: "tester@databricks.com", + }, nil + }) + + server.Handle("/api/2.0/workspace/get-status", func(r *http.Request) (any, error) { + return workspace.ObjectInfo{ + ObjectId: 1001, + ObjectType: "DIRECTORY", + Path: "", + ResourceId: "1001", + }, nil + }) +} diff --git a/bundle/tests/clusters_test.go b/bundle/tests/clusters_test.go deleted file mode 100644 index def8a2a31..000000000 --- a/bundle/tests/clusters_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package config_tests - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestClusters(t *testing.T) { - b := load(t, "./clusters") - assert.Equal(t, "clusters", b.Config.Bundle.Name) - - cluster := b.Config.Resources.Clusters["foo"] - assert.Equal(t, "foo", cluster.ClusterName) - assert.Equal(t, "13.3.x-scala2.12", cluster.SparkVersion) - assert.Equal(t, "i3.xlarge", cluster.NodeTypeId) - assert.Equal(t, 2, cluster.NumWorkers) - assert.Equal(t, "2g", cluster.SparkConf["spark.executor.memory"]) - assert.Equal(t, 2, cluster.Autoscale.MinWorkers) - assert.Equal(t, 7, cluster.Autoscale.MaxWorkers) -} - -func TestClustersOverride(t *testing.T) { - b := loadTarget(t, "./clusters", "development") - assert.Equal(t, "clusters", b.Config.Bundle.Name) - - cluster := b.Config.Resources.Clusters["foo"] - assert.Equal(t, "foo-override", cluster.ClusterName) - assert.Equal(t, "15.2.x-scala2.12", cluster.SparkVersion) - assert.Equal(t, "m5.xlarge", cluster.NodeTypeId) - assert.Equal(t, 3, cluster.NumWorkers) - assert.Equal(t, "4g", cluster.SparkConf["spark.executor.memory"]) - assert.Equal(t, "4g", cluster.SparkConf["spark.executor.memory2"]) - assert.Equal(t, 1, cluster.Autoscale.MinWorkers) - assert.Equal(t, 3, cluster.Autoscale.MaxWorkers) -} diff --git a/bundle/tests/complex_variables_test.go b/bundle/tests/complex_variables_test.go deleted file mode 100644 index d72b5f157..000000000 --- a/bundle/tests/complex_variables_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package config_tests - -import ( - "context" - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config/mutator" - "github.com/databricks/databricks-sdk-go/service/compute" - "github.com/stretchr/testify/require" -) - -func TestComplexVariables(t *testing.T) { - b, diags := loadTargetWithDiags("variables/complex", "default") - require.Empty(t, diags) - - diags = bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SetVariables(), - mutator.ResolveVariableReferencesInComplexVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - require.NoError(t, diags.Error()) - - require.Equal(t, "13.2.x-scala2.11", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.SparkVersion) - require.Equal(t, "Standard_DS3_v2", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.NodeTypeId) - require.Equal(t, "some-policy-id", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.PolicyId) - require.Equal(t, 2, b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.NumWorkers) - require.Equal(t, "true", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.SparkConf["spark.speculation"]) - require.Equal(t, "true", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.SparkConf["spark.random"]) - - require.Len(t, b.Config.Resources.Jobs["my_job"].Tasks[0].Libraries, 3) - require.Contains(t, b.Config.Resources.Jobs["my_job"].Tasks[0].Libraries, compute.Library{ - Jar: "/path/to/jar", - }) - require.Contains(t, b.Config.Resources.Jobs["my_job"].Tasks[0].Libraries, compute.Library{ - Egg: "/path/to/egg", - }) - require.Contains(t, b.Config.Resources.Jobs["my_job"].Tasks[0].Libraries, compute.Library{ - Whl: "/path/to/whl", - }) - - require.Equal(t, "task with spark version 13.2.x-scala2.11 and jar /path/to/jar", b.Config.Resources.Jobs["my_job"].Tasks[0].TaskKey) -} - -func TestComplexVariablesOverride(t *testing.T) { - b, diags := loadTargetWithDiags("variables/complex", "dev") - require.Empty(t, diags) - - diags = bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SetVariables(), - mutator.ResolveVariableReferencesInComplexVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - require.NoError(t, diags.Error()) - - require.Equal(t, "14.2.x-scala2.11", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.SparkVersion) - require.Equal(t, "Standard_DS3_v3", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.NodeTypeId) - require.Equal(t, 4, b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.NumWorkers) - require.Equal(t, "false", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.SparkConf["spark.speculation"]) - - // Making sure the variable is overriden and not merged / extended - // These properties are set in the default target but not set in override target - // So they should be empty - require.Equal(t, "", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.SparkConf["spark.random"]) - require.Equal(t, "", b.Config.Resources.Jobs["my_job"].JobClusters[0].NewCluster.PolicyId) -} - -func TestComplexVariablesOverrideWithMultipleFiles(t *testing.T) { - b, diags := loadTargetWithDiags("variables/complex_multiple_files", "dev") - require.Empty(t, diags) - - diags = bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SetVariables(), - mutator.ResolveVariableReferencesInComplexVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - require.NoError(t, diags.Error()) - for _, cluster := range b.Config.Resources.Jobs["my_job"].JobClusters { - require.Equalf(t, "14.2.x-scala2.11", cluster.NewCluster.SparkVersion, "cluster: %v", cluster.JobClusterKey) - require.Equalf(t, "Standard_DS3_v2", cluster.NewCluster.NodeTypeId, "cluster: %v", cluster.JobClusterKey) - require.Equalf(t, 4, cluster.NewCluster.NumWorkers, "cluster: %v", cluster.JobClusterKey) - require.Equalf(t, "false", cluster.NewCluster.SparkConf["spark.speculation"], "cluster: %v", cluster.JobClusterKey) - } -} - -func TestComplexVariablesOverrideWithFullSyntax(t *testing.T) { - b, diags := loadTargetWithDiags("variables/complex", "dev") - require.Empty(t, diags) - - diags = bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SetVariables(), - mutator.ResolveVariableReferencesInComplexVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - require.NoError(t, diags.Error()) - require.Empty(t, diags) - - complexvar := b.Config.Variables["complexvar"].Value - require.Equal(t, map[string]any{"key1": "1", "key2": "2", "key3": "3"}, complexvar) -} diff --git a/bundle/tests/override_job_cluster_test.go b/bundle/tests/override_job_cluster_test.go deleted file mode 100644 index 1393e03e5..000000000 --- a/bundle/tests/override_job_cluster_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package config_tests - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOverrideJobClusterDev(t *testing.T) { - b := loadTarget(t, "./override_job_cluster", "development") - assert.Equal(t, "job", b.Config.Resources.Jobs["foo"].Name) - assert.Len(t, b.Config.Resources.Jobs["foo"].JobClusters, 1) - - c := b.Config.Resources.Jobs["foo"].JobClusters[0] - assert.Equal(t, "13.3.x-scala2.12", c.NewCluster.SparkVersion) - assert.Equal(t, "i3.xlarge", c.NewCluster.NodeTypeId) - assert.Equal(t, 1, c.NewCluster.NumWorkers) -} - -func TestOverrideJobClusterStaging(t *testing.T) { - b := loadTarget(t, "./override_job_cluster", "staging") - assert.Equal(t, "job", b.Config.Resources.Jobs["foo"].Name) - assert.Len(t, b.Config.Resources.Jobs["foo"].JobClusters, 1) - - c := b.Config.Resources.Jobs["foo"].JobClusters[0] - assert.Equal(t, "13.3.x-scala2.12", c.NewCluster.SparkVersion) - assert.Equal(t, "i3.2xlarge", c.NewCluster.NodeTypeId) - assert.Equal(t, 4, c.NewCluster.NumWorkers) -} diff --git a/bundle/tests/override_job_tasks_test.go b/bundle/tests/override_job_tasks_test.go deleted file mode 100644 index 85463e17c..000000000 --- a/bundle/tests/override_job_tasks_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package config_tests - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOverrideTasksDev(t *testing.T) { - b := loadTarget(t, "./override_job_tasks", "development") - assert.Equal(t, "job", b.Config.Resources.Jobs["foo"].Name) - assert.Len(t, b.Config.Resources.Jobs["foo"].Tasks, 2) - - tasks := b.Config.Resources.Jobs["foo"].Tasks - assert.Equal(t, "key1", tasks[0].TaskKey) - assert.Equal(t, "i3.xlarge", tasks[0].NewCluster.NodeTypeId) - assert.Equal(t, 1, tasks[0].NewCluster.NumWorkers) - assert.Equal(t, "./test1.py", tasks[0].SparkPythonTask.PythonFile) - - assert.Equal(t, "key2", tasks[1].TaskKey) - assert.Equal(t, "13.3.x-scala2.12", tasks[1].NewCluster.SparkVersion) - assert.Equal(t, "./test2.py", tasks[1].SparkPythonTask.PythonFile) -} - -func TestOverrideTasksStaging(t *testing.T) { - b := loadTarget(t, "./override_job_tasks", "staging") - assert.Equal(t, "job", b.Config.Resources.Jobs["foo"].Name) - assert.Len(t, b.Config.Resources.Jobs["foo"].Tasks, 2) - - tasks := b.Config.Resources.Jobs["foo"].Tasks - assert.Equal(t, "key1", tasks[0].TaskKey) - assert.Equal(t, "13.3.x-scala2.12", tasks[0].NewCluster.SparkVersion) - assert.Equal(t, "./test1.py", tasks[0].SparkPythonTask.PythonFile) - - assert.Equal(t, "key2", tasks[1].TaskKey) - assert.Equal(t, "i3.2xlarge", tasks[1].NewCluster.NodeTypeId) - assert.Equal(t, 4, tasks[1].NewCluster.NumWorkers) - assert.Equal(t, "./test3.py", tasks[1].SparkPythonTask.PythonFile) -} diff --git a/bundle/tests/override_pipeline_cluster_test.go b/bundle/tests/override_pipeline_cluster_test.go deleted file mode 100644 index 591fe423d..000000000 --- a/bundle/tests/override_pipeline_cluster_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package config_tests - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOverridePipelineClusterDev(t *testing.T) { - b := loadTarget(t, "./override_pipeline_cluster", "development") - assert.Equal(t, "job", b.Config.Resources.Pipelines["foo"].Name) - assert.Len(t, b.Config.Resources.Pipelines["foo"].Clusters, 1) - - c := b.Config.Resources.Pipelines["foo"].Clusters[0] - assert.Equal(t, map[string]string{"foo": "bar"}, c.SparkConf) - assert.Equal(t, "i3.xlarge", c.NodeTypeId) - assert.Equal(t, 1, c.NumWorkers) -} - -func TestOverridePipelineClusterStaging(t *testing.T) { - b := loadTarget(t, "./override_pipeline_cluster", "staging") - assert.Equal(t, "job", b.Config.Resources.Pipelines["foo"].Name) - assert.Len(t, b.Config.Resources.Pipelines["foo"].Clusters, 1) - - c := b.Config.Resources.Pipelines["foo"].Clusters[0] - assert.Equal(t, map[string]string{"foo": "bar"}, c.SparkConf) - assert.Equal(t, "i3.2xlarge", c.NodeTypeId) - assert.Equal(t, 4, c.NumWorkers) -} diff --git a/bundle/tests/variables_test.go b/bundle/tests/variables_test.go deleted file mode 100644 index 37d488fad..000000000 --- a/bundle/tests/variables_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package config_tests - -import ( - "context" - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config/mutator" - "github.com/databricks/databricks-sdk-go/experimental/mocks" - "github.com/databricks/databricks-sdk-go/service/compute" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestVariables(t *testing.T) { - t.Setenv("BUNDLE_VAR_b", "def") - b := load(t, "./variables/vanilla") - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SetVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - require.NoError(t, diags.Error()) - assert.Equal(t, "abc def", b.Config.Bundle.Name) -} - -func TestVariablesLoadingFailsWhenRequiredVariableIsNotSpecified(t *testing.T) { - b := load(t, "./variables/vanilla") - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SetVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - assert.ErrorContains(t, diags.Error(), "no value assigned to required variable b. Assignment can be done through the \"--var\" flag or by setting the BUNDLE_VAR_b environment variable") -} - -func TestVariablesTargetsBlockOverride(t *testing.T) { - b := load(t, "./variables/env_overrides") - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SelectTarget("env-with-single-variable-override"), - mutator.SetVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - require.NoError(t, diags.Error()) - assert.Equal(t, "default-a dev-b", b.Config.Workspace.Profile) -} - -func TestVariablesTargetsBlockOverrideForMultipleVariables(t *testing.T) { - b := load(t, "./variables/env_overrides") - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SelectTarget("env-with-two-variable-overrides"), - mutator.SetVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - require.NoError(t, diags.Error()) - assert.Equal(t, "prod-a prod-b", b.Config.Workspace.Profile) -} - -func TestVariablesTargetsBlockOverrideWithProcessEnvVars(t *testing.T) { - t.Setenv("BUNDLE_VAR_b", "env-var-b") - b := load(t, "./variables/env_overrides") - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SelectTarget("env-with-two-variable-overrides"), - mutator.SetVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - require.NoError(t, diags.Error()) - assert.Equal(t, "prod-a env-var-b", b.Config.Workspace.Profile) -} - -func TestVariablesTargetsBlockOverrideWithMissingVariables(t *testing.T) { - b := load(t, "./variables/env_overrides") - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SelectTarget("env-missing-a-required-variable-assignment"), - mutator.SetVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - assert.ErrorContains(t, diags.Error(), "no value assigned to required variable b. Assignment can be done through the \"--var\" flag or by setting the BUNDLE_VAR_b environment variable") -} - -func TestVariablesTargetsBlockOverrideWithUndefinedVariables(t *testing.T) { - b := load(t, "./variables/env_overrides") - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SelectTarget("env-using-an-undefined-variable"), - mutator.SetVariables(), - mutator.ResolveVariableReferences( - "variables", - ), - )) - assert.ErrorContains(t, diags.Error(), "variable c is not defined but is assigned a value") -} - -func TestVariablesWithoutDefinition(t *testing.T) { - t.Setenv("BUNDLE_VAR_a", "foo") - t.Setenv("BUNDLE_VAR_b", "bar") - b := load(t, "./variables/without_definition") - diags := bundle.Apply(context.Background(), b, mutator.SetVariables()) - require.NoError(t, diags.Error()) - require.True(t, b.Config.Variables["a"].HasValue()) - require.True(t, b.Config.Variables["b"].HasValue()) - assert.Equal(t, "foo", b.Config.Variables["a"].Value) - assert.Equal(t, "bar", b.Config.Variables["b"].Value) -} - -func TestVariablesWithTargetLookupOverrides(t *testing.T) { - b := load(t, "./variables/env_overrides") - - mockWorkspaceClient := mocks.NewMockWorkspaceClient(t) - b.SetWorkpaceClient(mockWorkspaceClient.WorkspaceClient) - instancePoolApi := mockWorkspaceClient.GetMockInstancePoolsAPI() - instancePoolApi.EXPECT().GetByInstancePoolName(mock.Anything, "some-test-instance-pool").Return(&compute.InstancePoolAndStats{ - InstancePoolId: "1234", - }, nil) - - clustersApi := mockWorkspaceClient.GetMockClustersAPI() - clustersApi.EXPECT().ListAll(mock.Anything, compute.ListClustersRequest{ - FilterBy: &compute.ListClustersFilterBy{ - ClusterSources: []compute.ClusterSource{compute.ClusterSourceApi, compute.ClusterSourceUi}, - }, - }).Return([]compute.ClusterDetails{ - {ClusterId: "4321", ClusterName: "some-test-cluster"}, - {ClusterId: "9876", ClusterName: "some-other-cluster"}, - }, nil) - - clusterPoliciesApi := mockWorkspaceClient.GetMockClusterPoliciesAPI() - clusterPoliciesApi.EXPECT().GetByName(mock.Anything, "some-test-cluster-policy").Return(&compute.Policy{ - PolicyId: "9876", - }, nil) - - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SelectTarget("env-overrides-lookup"), - mutator.SetVariables(), - mutator.ResolveResourceReferences(), - )) - - require.NoError(t, diags.Error()) - assert.Equal(t, "4321", b.Config.Variables["d"].Value) - assert.Equal(t, "1234", b.Config.Variables["e"].Value) - assert.Equal(t, "9876", b.Config.Variables["f"].Value) -} - -func TestVariableTargetOverrides(t *testing.T) { - tcases := []struct { - targetName string - pipelineName string - pipelineContinuous bool - pipelineNumWorkers int - }{ - { - "use-default-variable-values", - "a_string", - true, - 42, - }, - { - "override-string-variable", - "overridden_string", - true, - 42, - }, - { - "override-int-variable", - "a_string", - true, - 43, - }, - { - "override-both-bool-and-string-variables", - "overridden_string", - false, - 42, - }, - } - - for _, tcase := range tcases { - t.Run(tcase.targetName, func(t *testing.T) { - b := loadTarget(t, "./variables/variable_overrides_in_target", tcase.targetName) - diags := bundle.Apply(context.Background(), b, bundle.Seq( - mutator.SetVariables(), - mutator.ResolveVariableReferences("variables")), - ) - require.NoError(t, diags.Error()) - - assert.Equal(t, tcase.pipelineName, b.Config.Resources.Pipelines["my_pipeline"].Name) - assert.Equal(t, tcase.pipelineContinuous, b.Config.Resources.Pipelines["my_pipeline"].Continuous) - assert.Equal(t, tcase.pipelineNumWorkers, b.Config.Resources.Pipelines["my_pipeline"].Clusters[0].NumWorkers) - }) - } -} - -func TestBundleWithEmptyVariableLoads(t *testing.T) { - b := load(t, "./variables/empty") - diags := bundle.Apply(context.Background(), b, mutator.SetVariables()) - require.ErrorContains(t, diags.Error(), "no value assigned to required variable a") -} diff --git a/libs/env/context.go b/libs/env/context.go index af4d1afa0..37b76147a 100644 --- a/libs/env/context.go +++ b/libs/env/context.go @@ -65,7 +65,7 @@ func Set(ctx context.Context, key, value string) context.Context { return setMap(ctx, m) } -func homeEnvVar() string { +func HomeEnvVar() string { if runtime.GOOS == "windows" { return "USERPROFILE" } @@ -73,14 +73,14 @@ func homeEnvVar() string { } func WithUserHomeDir(ctx context.Context, value string) context.Context { - return Set(ctx, homeEnvVar(), value) + return Set(ctx, HomeEnvVar(), value) } // ErrNoHomeEnv indicates the absence of $HOME env variable var ErrNoHomeEnv = errors.New("$HOME is not set") func UserHomeDir(ctx context.Context) (string, error) { - home := Get(ctx, homeEnvVar()) + home := Get(ctx, HomeEnvVar()) if home == "" { return "", ErrNoHomeEnv } diff --git a/libs/testdiff/testdiff.go b/libs/testdiff/testdiff.go index 71b781362..fef1d5ae2 100644 --- a/libs/testdiff/testdiff.go +++ b/libs/testdiff/testdiff.go @@ -25,7 +25,9 @@ func AssertEqualTexts(t testutil.TestingT, filename1, filename2, expected, out s } else { // only show diff for large texts diff := UnifiedDiff(filename1, filename2, expected, out) - t.Errorf("Diff:\n" + diff) + if diff != "" { + t.Errorf("Diff:\n" + diff) + } } } From 0289becea854dff5a89ee1c14809a3cc3cba998b Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Wed, 8 Jan 2025 13:43:56 +0100 Subject: [PATCH 36/53] Handle `${workspace.file_path}` references in source-linked deployments (#2046) ## Changes 1. Updates `workspace.file_path` during source-linked deployment to address cases like this https://github.com/databricks/bundle-examples/blob/main/default_python/resources/default_python_pipeline.yml#L13 2. Updates `workspace.file_path` in `metadata.json` 3. Prints warning for users when `workspace.file_path` is explicitly set but deploy is running in source-linked mode ## Tests Unit test --- bundle/config/mutator/apply_presets.go | 22 ---- bundle/config/mutator/apply_presets_test.go | 88 ------------- .../apply_source_linked_deployment_preset.go | 75 +++++++++++ ...ly_source_linked_deployment_preset_test.go | 122 ++++++++++++++++++ bundle/config/mutator/process_target_mode.go | 9 -- .../mutator/process_target_mode_test.go | 31 ----- .../mutator/resolve_variable_references.go | 25 ++-- .../resolve_variable_references_test.go | 55 ++++++++ bundle/deploy/metadata/compute.go | 3 + bundle/deploy/metadata/compute_test.go | 21 +++ bundle/phases/initialize.go | 5 + bundle/tests/loader.go | 3 +- libs/template/renderer_test.go | 3 +- 13 files changed, 301 insertions(+), 161 deletions(-) create mode 100644 bundle/config/mutator/apply_source_linked_deployment_preset.go create mode 100644 bundle/config/mutator/apply_source_linked_deployment_preset_test.go diff --git a/bundle/config/mutator/apply_presets.go b/bundle/config/mutator/apply_presets.go index 381703756..59b8547be 100644 --- a/bundle/config/mutator/apply_presets.go +++ b/bundle/config/mutator/apply_presets.go @@ -9,7 +9,6 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/libs/dbr" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn" "github.com/databricks/cli/libs/textutil" @@ -222,27 +221,6 @@ func (m *applyPresets) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnos dashboard.DisplayName = prefix + dashboard.DisplayName } - if config.IsExplicitlyEnabled((b.Config.Presets.SourceLinkedDeployment)) { - isDatabricksWorkspace := dbr.RunsOnRuntime(ctx) && strings.HasPrefix(b.SyncRootPath, "/Workspace/") - if !isDatabricksWorkspace { - target := b.Config.Bundle.Target - path := dyn.NewPath(dyn.Key("targets"), dyn.Key(target), dyn.Key("presets"), dyn.Key("source_linked_deployment")) - diags = diags.Append( - diag.Diagnostic{ - Severity: diag.Warning, - Summary: "source-linked deployment is available only in the Databricks Workspace", - Paths: []dyn.Path{ - path, - }, - Locations: b.Config.GetLocations(path[2:].String()), - }, - ) - - disabled := false - b.Config.Presets.SourceLinkedDeployment = &disabled - } - } - return diags } diff --git a/bundle/config/mutator/apply_presets_test.go b/bundle/config/mutator/apply_presets_test.go index c26f20383..5e3f942cc 100644 --- a/bundle/config/mutator/apply_presets_test.go +++ b/bundle/config/mutator/apply_presets_test.go @@ -2,16 +2,12 @@ package mutator_test import ( "context" - "runtime" "testing" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/bundle/internal/bundletest" - "github.com/databricks/cli/libs/dbr" - "github.com/databricks/cli/libs/dyn" "github.com/databricks/databricks-sdk-go/service/catalog" "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/stretchr/testify/require" @@ -398,87 +394,3 @@ func TestApplyPresetsResourceNotDefined(t *testing.T) { }) } } - -func TestApplyPresetsSourceLinkedDeployment(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("this test is not applicable on Windows because source-linked mode works only in the Databricks Workspace") - } - - testContext := context.Background() - enabled := true - disabled := false - workspacePath := "/Workspace/user.name@company.com" - - tests := []struct { - bundlePath string - ctx context.Context - name string - initialValue *bool - expectedValue *bool - expectedWarning string - }{ - { - name: "preset enabled, bundle in Workspace, databricks runtime", - bundlePath: workspacePath, - ctx: dbr.MockRuntime(testContext, true), - initialValue: &enabled, - expectedValue: &enabled, - }, - { - name: "preset enabled, bundle not in Workspace, databricks runtime", - bundlePath: "/Users/user.name@company.com", - ctx: dbr.MockRuntime(testContext, true), - initialValue: &enabled, - expectedValue: &disabled, - expectedWarning: "source-linked deployment is available only in the Databricks Workspace", - }, - { - name: "preset enabled, bundle in Workspace, not databricks runtime", - bundlePath: workspacePath, - ctx: dbr.MockRuntime(testContext, false), - initialValue: &enabled, - expectedValue: &disabled, - expectedWarning: "source-linked deployment is available only in the Databricks Workspace", - }, - { - name: "preset disabled, bundle in Workspace, databricks runtime", - bundlePath: workspacePath, - ctx: dbr.MockRuntime(testContext, true), - initialValue: &disabled, - expectedValue: &disabled, - }, - { - name: "preset nil, bundle in Workspace, databricks runtime", - bundlePath: workspacePath, - ctx: dbr.MockRuntime(testContext, true), - initialValue: nil, - expectedValue: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - b := &bundle.Bundle{ - SyncRootPath: tt.bundlePath, - Config: config.Root{ - Presets: config.Presets{ - SourceLinkedDeployment: tt.initialValue, - }, - }, - } - - bundletest.SetLocation(b, "presets.source_linked_deployment", []dyn.Location{{File: "databricks.yml"}}) - diags := bundle.Apply(tt.ctx, b, mutator.ApplyPresets()) - if diags.HasError() { - t.Fatalf("unexpected error: %v", diags) - } - - if tt.expectedWarning != "" { - require.Equal(t, tt.expectedWarning, diags[0].Summary) - require.NotEmpty(t, diags[0].Locations) - } - - require.Equal(t, tt.expectedValue, b.Config.Presets.SourceLinkedDeployment) - }) - } -} diff --git a/bundle/config/mutator/apply_source_linked_deployment_preset.go b/bundle/config/mutator/apply_source_linked_deployment_preset.go new file mode 100644 index 000000000..78ccc5322 --- /dev/null +++ b/bundle/config/mutator/apply_source_linked_deployment_preset.go @@ -0,0 +1,75 @@ +package mutator + +import ( + "context" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/libs/dbr" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" +) + +type applySourceLinkedDeploymentPreset struct{} + +// Apply source-linked deployment preset +func ApplySourceLinkedDeploymentPreset() *applySourceLinkedDeploymentPreset { + return &applySourceLinkedDeploymentPreset{} +} + +func (m *applySourceLinkedDeploymentPreset) Name() string { + return "ApplySourceLinkedDeploymentPreset" +} + +func (m *applySourceLinkedDeploymentPreset) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + if config.IsExplicitlyDisabled(b.Config.Presets.SourceLinkedDeployment) { + return nil + } + + var diags diag.Diagnostics + isDatabricksWorkspace := dbr.RunsOnRuntime(ctx) && strings.HasPrefix(b.SyncRootPath, "/Workspace/") + target := b.Config.Bundle.Target + + if config.IsExplicitlyEnabled((b.Config.Presets.SourceLinkedDeployment)) { + if !isDatabricksWorkspace { + path := dyn.NewPath(dyn.Key("targets"), dyn.Key(target), dyn.Key("presets"), dyn.Key("source_linked_deployment")) + diags = diags.Append( + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "source-linked deployment is available only in the Databricks Workspace", + Paths: []dyn.Path{ + path, + }, + Locations: b.Config.GetLocations(path[2:].String()), + }, + ) + + disabled := false + b.Config.Presets.SourceLinkedDeployment = &disabled + return diags + } + } + + if isDatabricksWorkspace && b.Config.Bundle.Mode == config.Development { + enabled := true + b.Config.Presets.SourceLinkedDeployment = &enabled + } + + if b.Config.Workspace.FilePath != "" && config.IsExplicitlyEnabled(b.Config.Presets.SourceLinkedDeployment) { + path := dyn.NewPath(dyn.Key("targets"), dyn.Key(target), dyn.Key("workspace"), dyn.Key("file_path")) + + diags = diags.Append( + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "workspace.file_path setting will be ignored in source-linked deployment mode", + Paths: []dyn.Path{ + path[2:], + }, + Locations: b.Config.GetLocations(path[2:].String()), + }, + ) + } + + return diags +} diff --git a/bundle/config/mutator/apply_source_linked_deployment_preset_test.go b/bundle/config/mutator/apply_source_linked_deployment_preset_test.go new file mode 100644 index 000000000..1b74fd8e9 --- /dev/null +++ b/bundle/config/mutator/apply_source_linked_deployment_preset_test.go @@ -0,0 +1,122 @@ +package mutator_test + +import ( + "context" + "runtime" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/mutator" + "github.com/databricks/cli/bundle/internal/bundletest" + "github.com/databricks/cli/libs/dbr" + "github.com/databricks/cli/libs/dyn" + "github.com/stretchr/testify/require" +) + +func TestApplyPresetsSourceLinkedDeployment(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("this test is not applicable on Windows because source-linked mode works only in the Databricks Workspace") + } + + testContext := context.Background() + enabled := true + disabled := false + workspacePath := "/Workspace/user.name@company.com" + + tests := []struct { + name string + ctx context.Context + mutateBundle func(b *bundle.Bundle) + initialValue *bool + expectedValue *bool + expectedWarning string + }{ + { + name: "preset enabled, bundle in Workspace, databricks runtime", + ctx: dbr.MockRuntime(testContext, true), + initialValue: &enabled, + expectedValue: &enabled, + }, + { + name: "preset enabled, bundle not in Workspace, databricks runtime", + ctx: dbr.MockRuntime(testContext, true), + mutateBundle: func(b *bundle.Bundle) { + b.SyncRootPath = "/Users/user.name@company.com" + }, + initialValue: &enabled, + expectedValue: &disabled, + expectedWarning: "source-linked deployment is available only in the Databricks Workspace", + }, + { + name: "preset enabled, bundle in Workspace, not databricks runtime", + ctx: dbr.MockRuntime(testContext, false), + initialValue: &enabled, + expectedValue: &disabled, + expectedWarning: "source-linked deployment is available only in the Databricks Workspace", + }, + { + name: "preset disabled, bundle in Workspace, databricks runtime", + ctx: dbr.MockRuntime(testContext, true), + initialValue: &disabled, + expectedValue: &disabled, + }, + { + name: "preset nil, bundle in Workspace, databricks runtime", + ctx: dbr.MockRuntime(testContext, true), + initialValue: nil, + expectedValue: nil, + }, + { + name: "preset nil, dev mode true, bundle in Workspace, databricks runtime", + ctx: dbr.MockRuntime(testContext, true), + mutateBundle: func(b *bundle.Bundle) { + b.Config.Bundle.Mode = config.Development + }, + initialValue: nil, + expectedValue: &enabled, + }, + { + name: "preset enabled, workspace.file_path is defined by user", + ctx: dbr.MockRuntime(testContext, true), + mutateBundle: func(b *bundle.Bundle) { + b.Config.Workspace.FilePath = "file_path" + }, + initialValue: &enabled, + expectedValue: &enabled, + expectedWarning: "workspace.file_path setting will be ignored in source-linked deployment mode", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := &bundle.Bundle{ + SyncRootPath: workspacePath, + Config: config.Root{ + Presets: config.Presets{ + SourceLinkedDeployment: tt.initialValue, + }, + }, + } + + if tt.mutateBundle != nil { + tt.mutateBundle(b) + } + + bundletest.SetLocation(b, "presets.source_linked_deployment", []dyn.Location{{File: "databricks.yml"}}) + bundletest.SetLocation(b, "workspace.file_path", []dyn.Location{{File: "databricks.yml"}}) + + diags := bundle.Apply(tt.ctx, b, mutator.ApplySourceLinkedDeploymentPreset()) + if diags.HasError() { + t.Fatalf("unexpected error: %v", diags) + } + + if tt.expectedWarning != "" { + require.Equal(t, tt.expectedWarning, diags[0].Summary) + require.NotEmpty(t, diags[0].Locations) + } + + require.Equal(t, tt.expectedValue, b.Config.Presets.SourceLinkedDeployment) + }) + } +} diff --git a/bundle/config/mutator/process_target_mode.go b/bundle/config/mutator/process_target_mode.go index df0136fad..44b53681d 100644 --- a/bundle/config/mutator/process_target_mode.go +++ b/bundle/config/mutator/process_target_mode.go @@ -6,7 +6,6 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/libs/dbr" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn" "github.com/databricks/cli/libs/iamutil" @@ -58,14 +57,6 @@ func transformDevelopmentMode(ctx context.Context, b *bundle.Bundle) { t.TriggerPauseStatus = config.Paused } - if !config.IsExplicitlyDisabled(t.SourceLinkedDeployment) { - isInWorkspace := strings.HasPrefix(b.SyncRootPath, "/Workspace/") - if isInWorkspace && dbr.RunsOnRuntime(ctx) { - enabled := true - t.SourceLinkedDeployment = &enabled - } - } - if !config.IsExplicitlyDisabled(t.PipelinesDevelopment) { enabled := true t.PipelinesDevelopment = &enabled diff --git a/bundle/config/mutator/process_target_mode_test.go b/bundle/config/mutator/process_target_mode_test.go index c299a7636..097c092a6 100644 --- a/bundle/config/mutator/process_target_mode_test.go +++ b/bundle/config/mutator/process_target_mode_test.go @@ -3,14 +3,12 @@ package mutator import ( "context" "reflect" - "runtime" "slices" "testing" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/libs/dbr" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/tags" "github.com/databricks/cli/libs/vfs" @@ -540,32 +538,3 @@ func TestPipelinesDevelopmentDisabled(t *testing.T) { assert.False(t, b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development) } - -func TestSourceLinkedDeploymentEnabled(t *testing.T) { - b, diags := processSourceLinkedBundle(t, true) - require.NoError(t, diags.Error()) - assert.True(t, *b.Config.Presets.SourceLinkedDeployment) -} - -func TestSourceLinkedDeploymentDisabled(t *testing.T) { - b, diags := processSourceLinkedBundle(t, false) - require.NoError(t, diags.Error()) - assert.False(t, *b.Config.Presets.SourceLinkedDeployment) -} - -func processSourceLinkedBundle(t *testing.T, presetEnabled bool) (*bundle.Bundle, diag.Diagnostics) { - if runtime.GOOS == "windows" { - t.Skip("this test is not applicable on Windows because source-linked mode works only in the Databricks Workspace") - } - - b := mockBundle(config.Development) - - workspacePath := "/Workspace/lennart@company.com/" - b.SyncRootPath = workspacePath - b.Config.Presets.SourceLinkedDeployment = &presetEnabled - - ctx := dbr.MockRuntime(context.Background(), true) - m := bundle.Seq(ProcessTargetMode(), ApplyPresets()) - diags := bundle.Apply(ctx, b, m) - return b, diags -} diff --git a/bundle/config/mutator/resolve_variable_references.go b/bundle/config/mutator/resolve_variable_references.go index e074c2b84..7ad3dfd8d 100644 --- a/bundle/config/mutator/resolve_variable_references.go +++ b/bundle/config/mutator/resolve_variable_references.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/variable" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn" @@ -15,7 +16,7 @@ import ( type resolveVariableReferences struct { prefixes []string pattern dyn.Pattern - lookupFn func(dyn.Value, dyn.Path) (dyn.Value, error) + lookupFn func(dyn.Value, dyn.Path, *bundle.Bundle) (dyn.Value, error) skipFn func(dyn.Value) bool } @@ -44,16 +45,21 @@ func ResolveVariableReferencesInComplexVariables() bundle.Mutator { } } -func lookup(v dyn.Value, path dyn.Path) (dyn.Value, error) { +func lookup(v dyn.Value, path dyn.Path, b *bundle.Bundle) (dyn.Value, error) { + if config.IsExplicitlyEnabled(b.Config.Presets.SourceLinkedDeployment) { + if path.String() == "workspace.file_path" { + return dyn.V(b.SyncRootPath), nil + } + } // Future opportunity: if we lookup this path in both the given root // and the synthesized root, we know if it was explicitly set or implied to be empty. // Then we can emit a warning if it was not explicitly set. return dyn.GetByPath(v, path) } -func lookupForComplexVariables(v dyn.Value, path dyn.Path) (dyn.Value, error) { +func lookupForComplexVariables(v dyn.Value, path dyn.Path, b *bundle.Bundle) (dyn.Value, error) { if path[0].Key() != "variables" { - return lookup(v, path) + return lookup(v, path, b) } varV, err := dyn.GetByPath(v, path[:len(path)-1]) @@ -71,7 +77,7 @@ func lookupForComplexVariables(v dyn.Value, path dyn.Path) (dyn.Value, error) { return dyn.InvalidValue, errors.New("complex variables cannot contain references to another complex variables") } - return lookup(v, path) + return lookup(v, path, b) } func skipResolvingInNonComplexVariables(v dyn.Value) bool { @@ -83,9 +89,9 @@ func skipResolvingInNonComplexVariables(v dyn.Value) bool { } } -func lookupForVariables(v dyn.Value, path dyn.Path) (dyn.Value, error) { +func lookupForVariables(v dyn.Value, path dyn.Path, b *bundle.Bundle) (dyn.Value, error) { if path[0].Key() != "variables" { - return lookup(v, path) + return lookup(v, path, b) } varV, err := dyn.GetByPath(v, path[:len(path)-1]) @@ -103,7 +109,7 @@ func lookupForVariables(v dyn.Value, path dyn.Path) (dyn.Value, error) { return dyn.InvalidValue, errors.New("lookup variables cannot contain references to another lookup variables") } - return lookup(v, path) + return lookup(v, path, b) } func (*resolveVariableReferences) Name() string { @@ -125,6 +131,7 @@ func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) varPath := dyn.NewPath(dyn.Key("var")) var diags diag.Diagnostics + err := b.Config.Mutate(func(root dyn.Value) (dyn.Value, error) { // Synthesize a copy of the root that has all fields that are present in the type // but not set in the dynamic value set to their corresponding empty value. @@ -167,7 +174,7 @@ func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) if m.skipFn != nil && m.skipFn(v) { return dyn.InvalidValue, dynvar.ErrSkipResolution } - return m.lookupFn(normalized, path) + return m.lookupFn(normalized, path, b) } } diff --git a/bundle/config/mutator/resolve_variable_references_test.go b/bundle/config/mutator/resolve_variable_references_test.go index 07972ecf4..fcad3180e 100644 --- a/bundle/config/mutator/resolve_variable_references_test.go +++ b/bundle/config/mutator/resolve_variable_references_test.go @@ -12,6 +12,7 @@ import ( "github.com/databricks/cli/libs/dyn" "github.com/databricks/databricks-sdk-go/service/compute" "github.com/databricks/databricks-sdk-go/service/jobs" + "github.com/databricks/databricks-sdk-go/service/pipelines" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -434,3 +435,57 @@ func TestResolveComplexVariableWithVarReference(t *testing.T) { require.NoError(t, diags.Error()) require.Equal(t, "cicd_template==1.0.0", b.Config.Resources.Jobs["job1"].JobSettings.Tasks[0].Libraries[0].Pypi.Package) } + +func TestResolveVariableReferencesWithSourceLinkedDeployment(t *testing.T) { + testCases := []struct { + enabled bool + assert func(t *testing.T, b *bundle.Bundle) + }{ + { + true, + func(t *testing.T, b *bundle.Bundle) { + // Variables that use workspace file path should have SyncRootValue during resolution phase + require.Equal(t, "sync/root/path", b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Configuration["source"]) + + // The file path itself should remain the same + require.Equal(t, "file/path", b.Config.Workspace.FilePath) + }, + }, + { + false, + func(t *testing.T, b *bundle.Bundle) { + require.Equal(t, "file/path", b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Configuration["source"]) + require.Equal(t, "file/path", b.Config.Workspace.FilePath) + }, + }, + } + + for _, testCase := range testCases { + b := &bundle.Bundle{ + SyncRootPath: "sync/root/path", + Config: config.Root{ + Presets: config.Presets{ + SourceLinkedDeployment: &testCase.enabled, + }, + Workspace: config.Workspace{ + FilePath: "file/path", + }, + Resources: config.Resources{ + Pipelines: map[string]*resources.Pipeline{ + "pipeline1": { + PipelineSpec: &pipelines.PipelineSpec{ + Configuration: map[string]string{ + "source": "${workspace.file_path}", + }, + }, + }, + }, + }, + }, + } + + diags := bundle.Apply(context.Background(), b, ResolveVariableReferences("workspace")) + require.NoError(t, diags.Error()) + testCase.assert(t, b) + } +} diff --git a/bundle/deploy/metadata/compute.go b/bundle/deploy/metadata/compute.go index bc8767de4..b47baa6b2 100644 --- a/bundle/deploy/metadata/compute.go +++ b/bundle/deploy/metadata/compute.go @@ -54,5 +54,8 @@ func (m *compute) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { // Set file upload destination of the bundle in metadata b.Metadata.Config.Workspace.FilePath = b.Config.Workspace.FilePath + if config.IsExplicitlyEnabled(b.Config.Presets.SourceLinkedDeployment) { + b.Metadata.Config.Workspace.FilePath = b.SyncRootPath + } return nil } diff --git a/bundle/deploy/metadata/compute_test.go b/bundle/deploy/metadata/compute_test.go index 2c2c72376..c6fa9bddb 100644 --- a/bundle/deploy/metadata/compute_test.go +++ b/bundle/deploy/metadata/compute_test.go @@ -97,3 +97,24 @@ func TestComputeMetadataMutator(t *testing.T) { assert.Equal(t, expectedMetadata, b.Metadata) } + +func TestComputeMetadataMutatorSourceLinked(t *testing.T) { + syncRootPath := "/Users/shreyas.goenka@databricks.com/source" + enabled := true + b := &bundle.Bundle{ + SyncRootPath: syncRootPath, + Config: config.Root{ + Presets: config.Presets{ + SourceLinkedDeployment: &enabled, + }, + Workspace: config.Workspace{ + FilePath: "/Users/shreyas.goenka@databricks.com/files", + }, + }, + } + + diags := bundle.Apply(context.Background(), b, Compute()) + require.NoError(t, diags.Error()) + + assert.Equal(t, syncRootPath, b.Metadata.Config.Workspace.FilePath) +} diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index f0cbc00c2..f7b3cd608 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -41,6 +41,10 @@ func Initialize() bundle.Mutator { mutator.PopulateCurrentUser(), mutator.LoadGitDetails(), + // This mutator needs to be run before variable interpolation and defining default workspace paths + // because it affects how workspace variables are resolved. + mutator.ApplySourceLinkedDeploymentPreset(), + mutator.DefineDefaultWorkspaceRoot(), mutator.ExpandWorkspaceRoot(), mutator.DefineDefaultWorkspacePaths(), @@ -51,6 +55,7 @@ func Initialize() bundle.Mutator { mutator.RewriteWorkspacePrefix(), mutator.SetVariables(), + // Intentionally placed before ResolveVariableReferencesInLookup, ResolveResourceReferences, // ResolveVariableReferencesInComplexVariables and ResolveVariableReferences. // See what is expected in PythonMutatorPhaseInit doc diff --git a/bundle/tests/loader.go b/bundle/tests/loader.go index 5c48d81cb..bb68b3059 100644 --- a/bundle/tests/loader.go +++ b/bundle/tests/loader.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/bundle/phases" + "github.com/databricks/cli/libs/dbr" "github.com/databricks/cli/libs/diag" "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/databricks-sdk-go/experimental/mocks" @@ -66,7 +67,7 @@ func initializeTarget(t *testing.T, path, env string) (*bundle.Bundle, diag.Diag b := load(t, path) configureMock(t, b) - ctx := context.Background() + ctx := dbr.MockRuntime(context.Background(), false) diags := bundle.Apply(ctx, b, bundle.Seq( mutator.SelectTarget(env), phases.Initialize(), diff --git a/libs/template/renderer_test.go b/libs/template/renderer_test.go index 70c8de12b..7ec1c5ced 100644 --- a/libs/template/renderer_test.go +++ b/libs/template/renderer_test.go @@ -16,6 +16,7 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/internal/testutil" + "github.com/databricks/cli/libs/dbr" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/filer" "github.com/databricks/cli/libs/tags" @@ -39,7 +40,7 @@ func assertFilePermissions(t *testing.T, path string, perm fs.FileMode) { } func assertBuiltinTemplateValid(t *testing.T, template string, settings map[string]any, target string, isServicePrincipal, build bool, tempDir string) { - ctx := context.Background() + ctx := dbr.MockRuntime(context.Background(), false) templateFS, err := fs.Sub(builtinTemplates, path.Join("templates", template)) require.NoError(t, err) From 23f05f5d67cbfc0b41a4ab60de101aaf4bd69ca4 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 8 Jan 2025 14:18:28 +0100 Subject: [PATCH 37/53] Set the write bit for files written during template initialization (#2068) ## Changes This used to work because the permission bits for built-in templates were hardcoded to 0644 for files and 0755 for directories. As of #1912 (and the PRs it depends on), built-in templates are no longer pre-materialized to a temporary directory and read directly from the embedded filesystem. This built-in filesystem returns 0444 as the permission bits for the files it contains. These bits are carried over to the destination filesystem. This change updates template materialization to always set the owner's write bit. It doesn't really make sense to write read-only files and expect users to work with these files in a VCS (note: Git only stores the executable bit). The regression shipped as part of v0.235.0 and will be fixed as of v0.238.0. ## Tests Unit tests. --- internal/testutil/file.go | 29 ++++++++++++++++++++++++++ libs/template/file_test.go | 9 +++++---- libs/template/renderer.go | 4 ++++ libs/template/renderer_test.go | 37 ++++++++++++++++++++-------------- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/internal/testutil/file.go b/internal/testutil/file.go index 538a3c20a..476c4123a 100644 --- a/internal/testutil/file.go +++ b/internal/testutil/file.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -52,3 +53,31 @@ func ReadFile(t TestingT, path string) string { return string(b) } + +// StatFile returns the file info for a file. +func StatFile(t TestingT, path string) os.FileInfo { + fi, err := os.Stat(path) + require.NoError(t, err) + + return fi +} + +// AssertFileContents asserts that the file at path has the expected content. +func AssertFileContents(t TestingT, path, expected string) bool { + actual := ReadFile(t, path) + return assert.Equal(t, expected, actual) +} + +// AssertFilePermissions asserts that the file at path has the expected permissions. +func AssertFilePermissions(t TestingT, path string, expected os.FileMode) bool { + fi := StatFile(t, path) + assert.False(t, fi.Mode().IsDir(), "expected a file, got a directory") + return assert.Equal(t, expected, fi.Mode().Perm(), "expected 0%o, got 0%o", expected, fi.Mode().Perm()) +} + +// AssertDirPermissions asserts that the file at path has the expected permissions. +func AssertDirPermissions(t TestingT, path string, expected os.FileMode) bool { + fi := StatFile(t, path) + assert.True(t, fi.Mode().IsDir(), "expected a directory, got a file") + return assert.Equal(t, expected, fi.Mode().Perm(), "expected 0%o, got 0%o", expected, fi.Mode().Perm()) +} diff --git a/libs/template/file_test.go b/libs/template/file_test.go index ced38c284..f4bf5652c 100644 --- a/libs/template/file_test.go +++ b/libs/template/file_test.go @@ -8,6 +8,7 @@ import ( "runtime" "testing" + "github.com/databricks/cli/internal/testutil" "github.com/databricks/cli/libs/filer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -27,8 +28,8 @@ func testInMemoryFile(t *testing.T, ctx context.Context, perm fs.FileMode) { err = f.Write(ctx, out) assert.NoError(t, err) - assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "123") - assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), perm) + testutil.AssertFileContents(t, filepath.Join(tmpDir, "a/b/c"), "123") + testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), perm) } func testCopyFile(t *testing.T, ctx context.Context, perm fs.FileMode) { @@ -48,8 +49,8 @@ func testCopyFile(t *testing.T, ctx context.Context, perm fs.FileMode) { err = f.Write(ctx, out) assert.NoError(t, err) - assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "qwerty") - assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), perm) + testutil.AssertFileContents(t, filepath.Join(tmpDir, "source"), "qwerty") + testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "source"), perm) } func TestTemplateInMemoryFilePersistToDisk(t *testing.T) { diff --git a/libs/template/renderer.go b/libs/template/renderer.go index 5030cd9df..679b7d8b7 100644 --- a/libs/template/renderer.go +++ b/libs/template/renderer.go @@ -150,6 +150,10 @@ func (r *renderer) computeFile(relPathTemplate string) (file, error) { } perm := info.Mode().Perm() + // Always include the write bit for the owner of the file. + // It does not make sense to have a file that is not writable by the owner. + perm |= 0o200 + // Execute relative path template to get destination path for the file relPath, err := r.executeTemplate(relPathTemplate) if err != nil { diff --git a/libs/template/renderer_test.go b/libs/template/renderer_test.go index 7ec1c5ced..b2ec388bd 100644 --- a/libs/template/renderer_test.go +++ b/libs/template/renderer_test.go @@ -27,16 +27,19 @@ import ( "github.com/stretchr/testify/require" ) -func assertFileContent(t *testing.T, path, content string) { - b, err := os.ReadFile(path) - require.NoError(t, err) - assert.Equal(t, content, string(b)) -} +var ( + defaultFilePermissions fs.FileMode + defaultDirPermissions fs.FileMode +) -func assertFilePermissions(t *testing.T, path string, perm fs.FileMode) { - info, err := os.Stat(path) - require.NoError(t, err) - assert.Equal(t, perm, info.Mode().Perm()) +func init() { + if runtime.GOOS == "windows" { + defaultFilePermissions = fs.FileMode(0o666) + defaultDirPermissions = fs.FileMode(0o777) + } else { + defaultFilePermissions = fs.FileMode(0o644) + defaultDirPermissions = fs.FileMode(0o755) + } } func assertBuiltinTemplateValid(t *testing.T, template string, settings map[string]any, target string, isServicePrincipal, build bool, tempDir string) { @@ -69,6 +72,10 @@ func assertBuiltinTemplateValid(t *testing.T, template string, settings map[stri err = renderer.persistToDisk(ctx, out) require.NoError(t, err) + // Verify permissions on file and directory + testutil.AssertFilePermissions(t, filepath.Join(tempDir, "my_project/README.md"), defaultFilePermissions) + testutil.AssertDirPermissions(t, filepath.Join(tempDir, "my_project/resources"), defaultDirPermissions) + b, err := bundle.Load(ctx, filepath.Join(tempDir, "my_project")) require.NoError(t, err) diags := bundle.Apply(ctx, b, phases.LoadNamedTarget(target)) @@ -347,10 +354,10 @@ func TestRendererPersistToDisk(t *testing.T) { assert.NoFileExists(t, filepath.Join(tmpDir, "a", "b", "c")) assert.NoFileExists(t, filepath.Join(tmpDir, "mno")) - assertFileContent(t, filepath.Join(tmpDir, "a", "b", "d"), "123") - assertFilePermissions(t, filepath.Join(tmpDir, "a", "b", "d"), 0o444) - assertFileContent(t, filepath.Join(tmpDir, "mmnn"), "456") - assertFilePermissions(t, filepath.Join(tmpDir, "mmnn"), 0o444) + testutil.AssertFileContents(t, filepath.Join(tmpDir, "a/b/d"), "123") + testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "a/b/d"), fs.FileMode(0o444)) + testutil.AssertFileContents(t, filepath.Join(tmpDir, "mmnn"), "456") + testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "mmnn"), fs.FileMode(0o444)) } func TestRendererWalk(t *testing.T) { @@ -617,8 +624,8 @@ func TestRendererFileTreeRendering(t *testing.T) { require.NoError(t, err) // Assert files and directories are correctly materialized. - assert.DirExists(t, filepath.Join(tmpDir, "my_directory")) - assert.FileExists(t, filepath.Join(tmpDir, "my_directory", "my_file")) + testutil.AssertDirPermissions(t, filepath.Join(tmpDir, "my_directory"), defaultDirPermissions) + testutil.AssertFilePermissions(t, filepath.Join(tmpDir, "my_directory", "my_file"), defaultFilePermissions) } func TestRendererSubTemplateInPath(t *testing.T) { From b48fa70984dc07273110c7309c239aa147d8bfbe Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 8 Jan 2025 15:47:38 +0100 Subject: [PATCH 38/53] [Release] Release v0.238.0 (#2096) Bundles: * Fix finding Python within virtualenv on Windows ([#2034](https://github.com/databricks/cli/pull/2034)). * Include missing field descriptions in JSON schema ([#2045](https://github.com/databricks/cli/pull/2045)). * Add validation for volume referenced from `artifact_path` ([#2050](https://github.com/databricks/cli/pull/2050)). * Handle `${workspace.file_path}` references in source-linked deployments ([#2046](https://github.com/databricks/cli/pull/2046)). * Set the write bit for files written during template initialization ([#2068](https://github.com/databricks/cli/pull/2068)). --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bdb0795b..5b59fa540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Version changelog +## [Release] Release v0.238.0 + +Bundles: + * Fix finding Python within virtualenv on Windows ([#2034](https://github.com/databricks/cli/pull/2034)). + * Include missing field descriptions in JSON schema ([#2045](https://github.com/databricks/cli/pull/2045)). + * Add validation for volume referenced from `artifact_path` ([#2050](https://github.com/databricks/cli/pull/2050)). + * Handle `${workspace.file_path}` references in source-linked deployments ([#2046](https://github.com/databricks/cli/pull/2046)). + * Set the write bit for files written during template initialization ([#2068](https://github.com/databricks/cli/pull/2068)). + ## [Release] Release v0.237.0 Bundles: From 42b34c7befcbd2f2cfc742bb61c72a0b0cdca5f8 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 8 Jan 2025 16:50:44 +0100 Subject: [PATCH 39/53] Update runner for the create-xyz-pr jobs in the release workflow (#2098) ## Changes I missed these in #2077 and they failed because of it on the v0.238.0 release. --- .github/workflows/release.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88e338a8c..061688506 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,8 +58,12 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} create-setup-cli-release-pr: + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + needs: goreleaser - runs-on: ubuntu-latest + steps: - name: Set VERSION variable from tag run: | @@ -82,8 +86,12 @@ jobs: }); create-homebrew-tap-release-pr: + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + needs: goreleaser - runs-on: ubuntu-latest + steps: - name: Set VERSION variable from tag run: | @@ -119,8 +127,12 @@ jobs: }); create-vscode-extension-update-pr: + runs-on: + group: databricks-deco-testing-runner-group + labels: ubuntu-latest-deco + needs: goreleaser - runs-on: ubuntu-latest + steps: - name: Set VERSION variable from tag run: | From df17e4b4ea1da0d46a61f07eef9a2c6d886a1c0d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 8 Jan 2025 18:44:52 +0100 Subject: [PATCH 40/53] Convert some resolve variables tests to acceptance test (#2100) --- .../variables/resolve-builtin/databricks.yml | 6 + .../variables/resolve-builtin/output.txt | 11 ++ .../bundle/variables/resolve-builtin/script | 1 + .../variables/resolve-empty/databricks.yml | 10 ++ .../bundle/variables/resolve-empty/output.txt | 3 + .../bundle/variables/resolve-empty/script | 1 + .../databricks.yml | 16 +++ .../resolve-field-within-complex/output.txt | 3 + .../resolve-field-within-complex/script | 1 + .../resolve_variable_references_test.go | 114 ------------------ 10 files changed, 52 insertions(+), 114 deletions(-) create mode 100644 acceptance/bundle/variables/resolve-builtin/databricks.yml create mode 100644 acceptance/bundle/variables/resolve-builtin/output.txt create mode 100644 acceptance/bundle/variables/resolve-builtin/script create mode 100644 acceptance/bundle/variables/resolve-empty/databricks.yml create mode 100644 acceptance/bundle/variables/resolve-empty/output.txt create mode 100644 acceptance/bundle/variables/resolve-empty/script create mode 100644 acceptance/bundle/variables/resolve-field-within-complex/databricks.yml create mode 100644 acceptance/bundle/variables/resolve-field-within-complex/output.txt create mode 100644 acceptance/bundle/variables/resolve-field-within-complex/script diff --git a/acceptance/bundle/variables/resolve-builtin/databricks.yml b/acceptance/bundle/variables/resolve-builtin/databricks.yml new file mode 100644 index 000000000..4bb71c8db --- /dev/null +++ b/acceptance/bundle/variables/resolve-builtin/databricks.yml @@ -0,0 +1,6 @@ +bundle: + name: TestResolveVariableReferences + +workspace: + root_path: "${bundle.name}/bar" + file_path: "${workspace.root_path}/baz" diff --git a/acceptance/bundle/variables/resolve-builtin/output.txt b/acceptance/bundle/variables/resolve-builtin/output.txt new file mode 100644 index 000000000..2f58abd8a --- /dev/null +++ b/acceptance/bundle/variables/resolve-builtin/output.txt @@ -0,0 +1,11 @@ +{ + "artifact_path": "TestResolveVariableReferences/bar/artifacts", + "current_user": { + "short_name": "tester", + "userName": "tester@databricks.com" + }, + "file_path": "TestResolveVariableReferences/bar/baz", + "resource_path": "TestResolveVariableReferences/bar/resources", + "root_path": "TestResolveVariableReferences/bar", + "state_path": "TestResolveVariableReferences/bar/state" +} diff --git a/acceptance/bundle/variables/resolve-builtin/script b/acceptance/bundle/variables/resolve-builtin/script new file mode 100644 index 000000000..fefd9abe6 --- /dev/null +++ b/acceptance/bundle/variables/resolve-builtin/script @@ -0,0 +1 @@ +$CLI bundle validate -o json | jq .workspace diff --git a/acceptance/bundle/variables/resolve-empty/databricks.yml b/acceptance/bundle/variables/resolve-empty/databricks.yml new file mode 100644 index 000000000..7563ada34 --- /dev/null +++ b/acceptance/bundle/variables/resolve-empty/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: TestResolveVariableReferencesToEmptyFields + git: + branch: "" + +resources: + jobs: + job1: + tags: + git_branch: "${bundle.git.branch}" diff --git a/acceptance/bundle/variables/resolve-empty/output.txt b/acceptance/bundle/variables/resolve-empty/output.txt new file mode 100644 index 000000000..a05cbbf54 --- /dev/null +++ b/acceptance/bundle/variables/resolve-empty/output.txt @@ -0,0 +1,3 @@ +{ + "git_branch": "" +} diff --git a/acceptance/bundle/variables/resolve-empty/script b/acceptance/bundle/variables/resolve-empty/script new file mode 100644 index 000000000..614673054 --- /dev/null +++ b/acceptance/bundle/variables/resolve-empty/script @@ -0,0 +1 @@ +$CLI bundle validate -o json | jq .resources.jobs.job1.tags diff --git a/acceptance/bundle/variables/resolve-field-within-complex/databricks.yml b/acceptance/bundle/variables/resolve-field-within-complex/databricks.yml new file mode 100644 index 000000000..7250dd5df --- /dev/null +++ b/acceptance/bundle/variables/resolve-field-within-complex/databricks.yml @@ -0,0 +1,16 @@ +bundle: + name: TestResolveComplexVariableReferencesToFields + +variables: + cluster: + type: "complex" + default: + node_type_id: "Standard_DS3_v2" + num_workers: 2 + +resources: + jobs: + job1: + job_clusters: + - new_cluster: + node_type_id: "${var.cluster.node_type_id}" diff --git a/acceptance/bundle/variables/resolve-field-within-complex/output.txt b/acceptance/bundle/variables/resolve-field-within-complex/output.txt new file mode 100644 index 000000000..1f6bdbbf4 --- /dev/null +++ b/acceptance/bundle/variables/resolve-field-within-complex/output.txt @@ -0,0 +1,3 @@ +{ + "node_type_id": "Standard_DS3_v2" +} diff --git a/acceptance/bundle/variables/resolve-field-within-complex/script b/acceptance/bundle/variables/resolve-field-within-complex/script new file mode 100644 index 000000000..a885870a5 --- /dev/null +++ b/acceptance/bundle/variables/resolve-field-within-complex/script @@ -0,0 +1 @@ +$CLI bundle validate -o json | jq .resources.jobs.job1.job_clusters[0].new_cluster diff --git a/bundle/config/mutator/resolve_variable_references_test.go b/bundle/config/mutator/resolve_variable_references_test.go index fcad3180e..18bb022aa 100644 --- a/bundle/config/mutator/resolve_variable_references_test.go +++ b/bundle/config/mutator/resolve_variable_references_test.go @@ -17,32 +17,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestResolveVariableReferences(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Bundle: config.Bundle{ - Name: "example", - }, - Workspace: config.Workspace{ - RootPath: "${bundle.name}/bar", - FilePath: "${workspace.root_path}/baz", - }, - }, - } - - // Apply with an invalid prefix. This should not change the workspace root path. - diags := bundle.Apply(context.Background(), b, ResolveVariableReferences("doesntexist")) - require.NoError(t, diags.Error()) - require.Equal(t, "${bundle.name}/bar", b.Config.Workspace.RootPath) - require.Equal(t, "${workspace.root_path}/baz", b.Config.Workspace.FilePath) - - // Apply with a valid prefix. This should change the workspace root path. - diags = bundle.Apply(context.Background(), b, ResolveVariableReferences("bundle", "workspace")) - require.NoError(t, diags.Error()) - require.Equal(t, "example/bar", b.Config.Workspace.RootPath) - require.Equal(t, "example/bar/baz", b.Config.Workspace.FilePath) -} - func TestResolveVariableReferencesToBundleVariables(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ @@ -66,37 +40,6 @@ func TestResolveVariableReferencesToBundleVariables(t *testing.T) { require.Equal(t, "example/bar", b.Config.Workspace.RootPath) } -func TestResolveVariableReferencesToEmptyFields(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Bundle: config.Bundle{ - Name: "example", - Git: config.Git{ - Branch: "", - }, - }, - Resources: config.Resources{ - Jobs: map[string]*resources.Job{ - "job1": { - JobSettings: &jobs.JobSettings{ - Tags: map[string]string{ - "git_branch": "${bundle.git.branch}", - }, - }, - }, - }, - }, - }, - } - - // Apply for the bundle prefix. - diags := bundle.Apply(context.Background(), b, ResolveVariableReferences("bundle")) - require.NoError(t, diags.Error()) - - // The job settings should have been interpolated to an empty string. - require.Equal(t, "", b.Config.Resources.Jobs["job1"].JobSettings.Tags["git_branch"]) -} - func TestResolveVariableReferencesForPrimitiveNonStringFields(t *testing.T) { var diags diag.Diagnostics @@ -251,63 +194,6 @@ func TestResolveComplexVariable(t *testing.T) { require.Equal(t, 2, b.Config.Resources.Jobs["job1"].JobSettings.JobClusters[0].NewCluster.NumWorkers) } -func TestResolveComplexVariableReferencesToFields(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Bundle: config.Bundle{ - Name: "example", - }, - Variables: map[string]*variable.Variable{ - "cluster": { - Value: map[string]any{ - "node_type_id": "Standard_DS3_v2", - "num_workers": 2, - }, - Type: variable.VariableTypeComplex, - }, - }, - - Resources: config.Resources{ - Jobs: map[string]*resources.Job{ - "job1": { - JobSettings: &jobs.JobSettings{ - JobClusters: []jobs.JobCluster{ - { - NewCluster: compute.ClusterSpec{ - NodeTypeId: "random", - }, - }, - }, - }, - }, - }, - }, - }, - } - - ctx := context.Background() - - // Assign the variables to the dynamic configuration. - diags := bundle.ApplyFunc(ctx, b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) { - var p dyn.Path - var err error - - p = dyn.MustPathFromString("resources.jobs.job1.job_clusters[0].new_cluster") - v, err = dyn.SetByPath(v, p.Append(dyn.Key("node_type_id")), dyn.V("${var.cluster.node_type_id}")) - require.NoError(t, err) - - return v, nil - }) - return diag.FromErr(err) - }) - require.NoError(t, diags.Error()) - - diags = bundle.Apply(ctx, b, ResolveVariableReferences("bundle", "workspace", "variables")) - require.NoError(t, diags.Error()) - require.Equal(t, "Standard_DS3_v2", b.Config.Resources.Jobs["job1"].JobSettings.JobClusters[0].NewCluster.NodeTypeId) -} - func TestResolveComplexVariableReferencesWithComplexVariablesError(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ From b0706ccdc129291714d46de408fbed4c7baca733 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 9 Jan 2025 10:00:05 +0100 Subject: [PATCH 41/53] Use -update instead of TESTS_OUTPUT=OVERWRITE (#2097) It's easier to remember and type and also validated and part of help: ``` % go test ./acceptance -updat 2>&1 | grep updat flag provided but not defined: -updat -update ``` --- acceptance/README.md | 2 +- libs/testdiff/golden.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/acceptance/README.md b/acceptance/README.md index 162c57ea2..42a37d253 100644 --- a/acceptance/README.md +++ b/acceptance/README.md @@ -9,7 +9,7 @@ To author a test, The test runner will run script and capture output and compare it with `output.txt` file in the same directory. -In order to write `output.txt` for the first time or overwrite it with the current output, set `TESTS_OUTPUT=OVERWRITE` env var. +In order to write `output.txt` for the first time or overwrite it with the current output pass -update flag to go test. The scripts are run with `bash -e` so any errors will be propagated. They are captured in `output.txt` by appending `Exit code: N` line at the end. diff --git a/libs/testdiff/golden.go b/libs/testdiff/golden.go index b67eb50a9..02213c88a 100644 --- a/libs/testdiff/golden.go +++ b/libs/testdiff/golden.go @@ -2,6 +2,7 @@ package testdiff import ( "context" + "flag" "fmt" "os" "regexp" @@ -16,7 +17,11 @@ import ( "github.com/stretchr/testify/assert" ) -var OverwriteMode = os.Getenv("TESTS_OUTPUT") == "OVERWRITE" +var OverwriteMode = false + +func init() { + flag.BoolVar(&OverwriteMode, "update", false, "Overwrite golden files") +} func ReadFile(t testutil.TestingT, ctx context.Context, filename string) string { t.Helper() From b808d4d6f1b860f6286b2988a6226c5d506910b3 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 9 Jan 2025 10:03:03 +0100 Subject: [PATCH 42/53] Add test for overriding list variable (#2099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add override for "libraries". - Remove complexvar - it serves no purpose - we already have map variable ‘cluster’. --- .../bundle/variables/complex/databricks.yml | 19 +++------- .../bundle/variables/complex/out.default.json | 14 ------- .../bundle/variables/complex/out.dev.json | 37 ++++--------------- .../bundle/variables/complex/output.txt | 14 +++++++ acceptance/bundle/variables/complex/script | 10 +++-- 5 files changed, 33 insertions(+), 61 deletions(-) create mode 100644 acceptance/bundle/variables/complex/output.txt diff --git a/acceptance/bundle/variables/complex/databricks.yml b/acceptance/bundle/variables/complex/databricks.yml index 500f374e3..5dcc30b08 100644 --- a/acceptance/bundle/variables/complex/databricks.yml +++ b/acceptance/bundle/variables/complex/databricks.yml @@ -11,6 +11,7 @@ resources: - task_key: test job_cluster_key: key libraries: ${variables.libraries.value} + # specific fields of complex variable are referenced: task_key: "task with spark version ${var.cluster.spark_version} and jar ${var.libraries[0].jar}" variables: @@ -35,14 +36,6 @@ variables: - jar: "/path/to/jar" - egg: "/path/to/egg" - whl: "/path/to/whl" - complexvar: - type: complex - description: "A complex variable" - default: - key1: "value1" - key2: "value2" - key3: "value3" - targets: default: @@ -51,15 +44,13 @@ targets: variables: node_type: "Standard_DS3_v3" cluster: + # complex variables are not merged, so missing variables (policy_id) are not inherited spark_version: "14.2.x-scala2.11" node_type_id: ${var.node_type} num_workers: 4 spark_conf: spark.speculation: false spark.databricks.delta.retentionDurationCheck.enabled: false - complexvar: - type: complex - default: - key1: "1" - key2: "2" - key3: "3" + libraries: + - jar: "/newpath/to/jar" + - whl: "/newpath/to/whl" diff --git a/acceptance/bundle/variables/complex/out.default.json b/acceptance/bundle/variables/complex/out.default.json index bbdb0f8de..6454562a6 100644 --- a/acceptance/bundle/variables/complex/out.default.json +++ b/acceptance/bundle/variables/complex/out.default.json @@ -76,20 +76,6 @@ "spark_version": "13.2.x-scala2.11" } }, - "complexvar": { - "default": { - "key1": "value1", - "key2": "value2", - "key3": "value3" - }, - "description": "A complex variable", - "type": "complex", - "value": { - "key1": "value1", - "key2": "value2", - "key3": "value3" - } - }, "libraries": { "default": [ { diff --git a/acceptance/bundle/variables/complex/out.dev.json b/acceptance/bundle/variables/complex/out.dev.json index 330518c85..cede5feb2 100644 --- a/acceptance/bundle/variables/complex/out.dev.json +++ b/acceptance/bundle/variables/complex/out.dev.json @@ -32,16 +32,13 @@ "job_cluster_key": "key", "libraries": [ { - "jar": "/path/to/jar" + "jar": "/newpath/to/jar" }, { - "egg": "/path/to/egg" - }, - { - "whl": "/path/to/whl" + "whl": "/newpath/to/whl" } ], - "task_key": "task with spark version 14.2.x-scala2.11 and jar /path/to/jar" + "task_key": "task with spark version 14.2.x-scala2.11 and jar /newpath/to/jar" } ] } @@ -70,43 +67,23 @@ "spark_version": "14.2.x-scala2.11" } }, - "complexvar": { - "default": { - "key1": "1", - "key2": "2", - "key3": "3" - }, - "description": "A complex variable", - "type": "complex", - "value": { - "key1": "1", - "key2": "2", - "key3": "3" - } - }, "libraries": { "default": [ { - "jar": "/path/to/jar" + "jar": "/newpath/to/jar" }, { - "egg": "/path/to/egg" - }, - { - "whl": "/path/to/whl" + "whl": "/newpath/to/whl" } ], "description": "A libraries definition", "type": "complex", "value": [ { - "jar": "/path/to/jar" + "jar": "/newpath/to/jar" }, { - "egg": "/path/to/egg" - }, - { - "whl": "/path/to/whl" + "whl": "/newpath/to/whl" } ] }, diff --git a/acceptance/bundle/variables/complex/output.txt b/acceptance/bundle/variables/complex/output.txt new file mode 100644 index 000000000..ce295421f --- /dev/null +++ b/acceptance/bundle/variables/complex/output.txt @@ -0,0 +1,14 @@ + +>>> $CLI bundle validate -o json + +>>> jq .resources.jobs.my_job.tasks[0].task_key out.default.json +"task with spark version 13.2.x-scala2.11 and jar /path/to/jar" + +>>> $CLI bundle validate -o json -t dev + +>>> jq .resources.jobs.my_job.tasks[0].task_key out.dev.json +"task with spark version 14.2.x-scala2.11 and jar /newpath/to/jar" +policy_id and spark_conf.spark_random fields do not exist in dev target: + +>>> jq .resources.jobs.my_job.job_clusters[0].new_cluster.policy_id out.dev.json +null diff --git a/acceptance/bundle/variables/complex/script b/acceptance/bundle/variables/complex/script index 9ee0ab02a..f8b61f18d 100644 --- a/acceptance/bundle/variables/complex/script +++ b/acceptance/bundle/variables/complex/script @@ -1,4 +1,8 @@ -$CLI bundle validate -o json | jq '{resources,variables}' > out.default.json +trace $CLI bundle validate -o json | jq '{resources,variables}' > out.default.json +trace jq .resources.jobs.my_job.tasks[0].task_key out.default.json | grep "task with spark version 13.2.x-scala2.11 and jar /path/to/jar" -# spark.random and policy_id should be empty in this target: -$CLI bundle validate -o json -t dev | jq '{resources,variables}' > out.dev.json +trace $CLI bundle validate -o json -t dev | jq '{resources,variables}' > out.dev.json +trace jq .resources.jobs.my_job.tasks[0].task_key out.dev.json | grep "task with spark version 14.2.x-scala2.11 and jar /newpath/to/jar" + +echo policy_id and spark_conf.spark_random fields do not exist in dev target: +trace jq .resources.jobs.my_job.job_clusters[0].new_cluster.policy_id out.dev.json | grep null From 2a4fdd911e2b8ece6a9675bfa409ade9343847cb Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 9 Jan 2025 11:14:47 +0100 Subject: [PATCH 43/53] Add a test showing bug in merge if variables are used for keys (#2101) Since merge happens first, before variable resolution, the two jobs are seen as different. I also updated override/job_cluster/script to include more of the output. --- .../bundle/override/job_cluster/output.txt | 66 ++++++++++----- acceptance/bundle/override/job_cluster/script | 4 +- .../override/job_cluster_var/databricks.yml | 37 ++++++++ .../override/job_cluster_var/output.txt | 84 +++++++++++++++++++ .../bundle/override/job_cluster_var/script | 4 + 5 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 acceptance/bundle/override/job_cluster_var/databricks.yml create mode 100644 acceptance/bundle/override/job_cluster_var/output.txt create mode 100644 acceptance/bundle/override/job_cluster_var/script diff --git a/acceptance/bundle/override/job_cluster/output.txt b/acceptance/bundle/override/job_cluster/output.txt index dc7a5f75b..947d19032 100644 --- a/acceptance/bundle/override/job_cluster/output.txt +++ b/acceptance/bundle/override/job_cluster/output.txt @@ -1,30 +1,56 @@ >>> $CLI bundle validate -o json -t development { - "name": "job", - "job_clusters": [ - { - "job_cluster_key": "key", - "new_cluster": { - "node_type_id": "i3.xlarge", - "num_workers": 1, - "spark_version": "13.3.x-scala2.12" + "foo": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/override_job_cluster/development/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "job_clusters": [ + { + "job_cluster_key": "key", + "new_cluster": { + "node_type_id": "i3.xlarge", + "num_workers": 1, + "spark_version": "13.3.x-scala2.12" + } } - } - ] + ], + "name": "job", + "permissions": [], + "queue": { + "enabled": true + }, + "tags": {} + } } >>> $CLI bundle validate -o json -t staging { - "name": "job", - "job_clusters": [ - { - "job_cluster_key": "key", - "new_cluster": { - "node_type_id": "i3.2xlarge", - "num_workers": 4, - "spark_version": "13.3.x-scala2.12" + "foo": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/override_job_cluster/staging/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "job_clusters": [ + { + "job_cluster_key": "key", + "new_cluster": { + "node_type_id": "i3.2xlarge", + "num_workers": 4, + "spark_version": "13.3.x-scala2.12" + } } - } - ] + ], + "name": "job", + "permissions": [], + "queue": { + "enabled": true + }, + "tags": {} + } } diff --git a/acceptance/bundle/override/job_cluster/script b/acceptance/bundle/override/job_cluster/script index 3f6827bb7..4a26c433a 100644 --- a/acceptance/bundle/override/job_cluster/script +++ b/acceptance/bundle/override/job_cluster/script @@ -1,2 +1,2 @@ -trace $CLI bundle validate -o json -t development | jq '.resources.jobs.foo | {name,job_clusters}' -trace $CLI bundle validate -o json -t staging | jq '.resources.jobs.foo | {name,job_clusters}' +trace $CLI bundle validate -o json -t development | jq '.resources.jobs' +trace $CLI bundle validate -o json -t staging | jq '.resources.jobs' diff --git a/acceptance/bundle/override/job_cluster_var/databricks.yml b/acceptance/bundle/override/job_cluster_var/databricks.yml new file mode 100644 index 000000000..546cc2d8a --- /dev/null +++ b/acceptance/bundle/override/job_cluster_var/databricks.yml @@ -0,0 +1,37 @@ +bundle: + name: override_job_cluster + +variables: + mykey: + default: key + +resources: + jobs: + foo: + name: job + job_clusters: + - job_cluster_key: key + new_cluster: + spark_version: 13.3.x-scala2.12 + +targets: + development: + resources: + jobs: + foo: + job_clusters: + # This does not work because merging is done before resolution + - job_cluster_key: "${var.mykey}" + new_cluster: + node_type_id: i3.xlarge + num_workers: 1 + + staging: + resources: + jobs: + foo: + job_clusters: + - job_cluster_key: "${var.mykey}" + new_cluster: + node_type_id: i3.2xlarge + num_workers: 4 diff --git a/acceptance/bundle/override/job_cluster_var/output.txt b/acceptance/bundle/override/job_cluster_var/output.txt new file mode 100644 index 000000000..dee2a3b5b --- /dev/null +++ b/acceptance/bundle/override/job_cluster_var/output.txt @@ -0,0 +1,84 @@ + +>>> $CLI bundle validate -o json -t development +{ + "foo": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/override_job_cluster/development/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "job_clusters": [ + { + "job_cluster_key": "key", + "new_cluster": { + "spark_version": "13.3.x-scala2.12" + } + }, + { + "job_cluster_key": "key", + "new_cluster": { + "node_type_id": "i3.xlarge", + "num_workers": 1 + } + } + ], + "name": "job", + "permissions": [], + "queue": { + "enabled": true + }, + "tags": {} + } +} + +>>> $CLI bundle validate -t development +Name: override_job_cluster +Target: development +Workspace: + User: tester@databricks.com + Path: /Workspace/Users/tester@databricks.com/.bundle/override_job_cluster/development + +Validation OK! + +>>> $CLI bundle validate -o json -t staging +{ + "foo": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/override_job_cluster/staging/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "job_clusters": [ + { + "job_cluster_key": "key", + "new_cluster": { + "spark_version": "13.3.x-scala2.12" + } + }, + { + "job_cluster_key": "key", + "new_cluster": { + "node_type_id": "i3.2xlarge", + "num_workers": 4 + } + } + ], + "name": "job", + "permissions": [], + "queue": { + "enabled": true + }, + "tags": {} + } +} + +>>> $CLI bundle validate -t staging +Name: override_job_cluster +Target: staging +Workspace: + User: tester@databricks.com + Path: /Workspace/Users/tester@databricks.com/.bundle/override_job_cluster/staging + +Validation OK! diff --git a/acceptance/bundle/override/job_cluster_var/script b/acceptance/bundle/override/job_cluster_var/script new file mode 100644 index 000000000..1cf373828 --- /dev/null +++ b/acceptance/bundle/override/job_cluster_var/script @@ -0,0 +1,4 @@ +trace $CLI bundle validate -o json -t development | jq '.resources.jobs' +trace $CLI bundle validate -t development +trace $CLI bundle validate -o json -t staging | jq '.resources.jobs' +trace $CLI bundle validate -t staging From 3b3ede6e31181396c46a6ddb86299566e23b75fe Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 9 Jan 2025 12:21:30 +0100 Subject: [PATCH 44/53] Update runner for the publish-winget job (#2105) ## Changes This action uses a token to access the release artifacts and, as such, needs to execute on the runner that's on the allowlist. Related PRs: * #2098 * #2077 --- .github/workflows/publish-winget.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-winget.yml b/.github/workflows/publish-winget.yml index 19603e669..dd3aebcbd 100644 --- a/.github/workflows/publish-winget.yml +++ b/.github/workflows/publish-winget.yml @@ -5,8 +5,12 @@ on: jobs: publish-to-winget-pkgs: - runs-on: windows-latest + runs-on: + group: databricks-protected-runner-group + labels: windows-server-latest + environment: release + steps: - uses: vedantmgoyal2009/winget-releaser@93fd8b606a1672ec3e5c6c3bb19426be68d1a8b0 # https://github.com/vedantmgoyal2009/winget-releaser/releases/tag/v2 with: From 4b67e9f33611089f177ad68431ee2070a195261b Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 9 Jan 2025 13:07:29 +0100 Subject: [PATCH 45/53] Pass tag to release as input to publish-winget workflow (#2107) ## Changes This workflow only worked if it was triggered on the tag to publish itself. This means it is not possible to release a version if the workflow configuration at that tag is broken (as is the case for v0.238.0 because of #2105). This change adds a "tag" input that can be set when manually triggering the workflow. ## Tests * Succesful run with this change: https://github.com/databricks/cli/actions/runs/12689281843 * Pull request that the run created: https://github.com/microsoft/winget-pkgs/pull/209220 --- .github/workflows/publish-winget.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/publish-winget.yml b/.github/workflows/publish-winget.yml index dd3aebcbd..267077102 100644 --- a/.github/workflows/publish-winget.yml +++ b/.github/workflows/publish-winget.yml @@ -2,6 +2,10 @@ name: publish-winget on: workflow_dispatch: + inputs: + tag: + description: 'Tag to publish' + default: '' jobs: publish-to-winget-pkgs: @@ -18,3 +22,7 @@ jobs: installers-regex: 'windows_.*-signed\.zip$' # Only signed Windows releases token: ${{ secrets.ENG_DEV_ECOSYSTEM_BOT_TOKEN }} fork-user: eng-dev-ecosystem-bot + + # Use the tag from the input, or the ref name if the input is not provided. + # The ref name is equal to the tag name when this workflow is triggered by the "sign-cli" command. + release-tag: ${{ inputs.tag || github.ref_name }} From a0455bcaefa488ca736b0baba0573da7dceeb30d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 9 Jan 2025 16:21:24 +0100 Subject: [PATCH 46/53] Migrate bundle/tests/undefined_resources_test.go to acceptance test (#2106) Add sort_blocks.py helper to deal with non-determinism. --- acceptance/acceptance_test.go | 12 +++-- acceptance/bin/sort_blocks.py | 21 ++++++++ .../undefined_resources/databricks.yml | 0 .../bundle/undefined_resources/output.txt | 19 +++++++ acceptance/bundle/undefined_resources/script | 2 + bundle/tests/undefined_resources_test.go | 50 ------------------- 6 files changed, 50 insertions(+), 54 deletions(-) create mode 100755 acceptance/bin/sort_blocks.py rename {bundle/tests => acceptance/bundle}/undefined_resources/databricks.yml (100%) create mode 100644 acceptance/bundle/undefined_resources/output.txt create mode 100644 acceptance/bundle/undefined_resources/script delete mode 100644 bundle/tests/undefined_resources_test.go diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 759c0aeca..033f26dfb 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -35,10 +35,16 @@ var Scripts = map[string]bool{ } func TestAccept(t *testing.T) { - execPath := BuildCLI(t) + cwd, err := os.Getwd() + require.NoError(t, err) + + execPath := BuildCLI(t, cwd) // $CLI is what test scripts are using t.Setenv("CLI", execPath) + // Make helper scripts available + t.Setenv("PATH", fmt.Sprintf("%s%c%s", filepath.Join(cwd, "bin"), os.PathListSeparator, os.Getenv("PATH"))) + server := StartServer(t) AddHandlers(server) // Redirect API access to local server: @@ -199,9 +205,7 @@ func readMergedScriptContents(t *testing.T, dir string) string { return strings.Join(prepares, "\n") } -func BuildCLI(t *testing.T) string { - cwd, err := os.Getwd() - require.NoError(t, err) +func BuildCLI(t *testing.T, cwd string) string { execPath := filepath.Join(cwd, "build", "databricks") if runtime.GOOS == "windows" { execPath += ".exe" diff --git a/acceptance/bin/sort_blocks.py b/acceptance/bin/sort_blocks.py new file mode 100755 index 000000000..f50c6f50f --- /dev/null +++ b/acceptance/bin/sort_blocks.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +""" +Helper to sort blocks in text file. A block is a set of lines separated from others by empty line. + +This is to workaround non-determinism in the output. +""" +import sys + +blocks = [] + +for line in sys.stdin: + if not line.strip(): + if blocks and blocks[-1]: + blocks.append('') + continue + if not blocks: + blocks.append('') + blocks[-1] += line + +blocks.sort() +print("\n".join(blocks)) diff --git a/bundle/tests/undefined_resources/databricks.yml b/acceptance/bundle/undefined_resources/databricks.yml similarity index 100% rename from bundle/tests/undefined_resources/databricks.yml rename to acceptance/bundle/undefined_resources/databricks.yml diff --git a/acceptance/bundle/undefined_resources/output.txt b/acceptance/bundle/undefined_resources/output.txt new file mode 100644 index 000000000..29b51bc1a --- /dev/null +++ b/acceptance/bundle/undefined_resources/output.txt @@ -0,0 +1,19 @@ +Error: experiment undefined-experiment is not defined + at resources.experiments.undefined-experiment + in databricks.yml:11:26 + +Error: job undefined-job is not defined + at resources.jobs.undefined-job + in databricks.yml:6:19 + +Error: pipeline undefined-pipeline is not defined + at resources.pipelines.undefined-pipeline + in databricks.yml:14:24 + +Found 3 errors + +Name: undefined-job +Target: default + + +Exit code: 1 diff --git a/acceptance/bundle/undefined_resources/script b/acceptance/bundle/undefined_resources/script new file mode 100644 index 000000000..10a3c485a --- /dev/null +++ b/acceptance/bundle/undefined_resources/script @@ -0,0 +1,2 @@ +# We need sort_blocks.py because the order of diagnostics is currently randomized +$CLI bundle validate 2>&1 | sort_blocks.py diff --git a/bundle/tests/undefined_resources_test.go b/bundle/tests/undefined_resources_test.go deleted file mode 100644 index 3dbacbc25..000000000 --- a/bundle/tests/undefined_resources_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package config_tests - -import ( - "context" - "path/filepath" - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config/validate" - "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/dyn" - "github.com/stretchr/testify/assert" -) - -func TestUndefinedResourcesLoadWithError(t *testing.T) { - b := load(t, "./undefined_resources") - diags := bundle.Apply(context.Background(), b, validate.AllResourcesHaveValues()) - - assert.Len(t, diags, 3) - assert.Contains(t, diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "job undefined-job is not defined", - Locations: []dyn.Location{{ - File: filepath.FromSlash("undefined_resources/databricks.yml"), - Line: 6, - Column: 19, - }}, - Paths: []dyn.Path{dyn.MustPathFromString("resources.jobs.undefined-job")}, - }) - assert.Contains(t, diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "experiment undefined-experiment is not defined", - Locations: []dyn.Location{{ - File: filepath.FromSlash("undefined_resources/databricks.yml"), - Line: 11, - Column: 26, - }}, - Paths: []dyn.Path{dyn.MustPathFromString("resources.experiments.undefined-experiment")}, - }) - assert.Contains(t, diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "pipeline undefined-pipeline is not defined", - Locations: []dyn.Location{{ - File: filepath.FromSlash("undefined_resources/databricks.yml"), - Line: 14, - Column: 24, - }}, - Paths: []dyn.Path{dyn.MustPathFromString("resources.pipelines.undefined-pipeline")}, - }) -} From b0c1c236307b19a7fa8df472756e48c3e8f47897 Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Thu, 9 Jan 2025 23:49:34 +0530 Subject: [PATCH 47/53] Add `uuid` to builtin templates (#2088) ## Changes This is useful to track telemetry associated with the templates and can later be useful for functional usecases as well. Mlops stacks does the same here: https://github.com/databricks/mlops-stacks/pull/185 ## Tests Existing tests. --- integration/bundle/testdata/default_python/bundle_summary.txt | 3 ++- .../dbt-sql/template/{{.project_name}}/databricks.yml.tmpl | 1 + .../template/{{.project_name}}/databricks.yml.tmpl | 1 + .../default-sql/template/{{.project_name}}/databricks.yml.tmpl | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/integration/bundle/testdata/default_python/bundle_summary.txt b/integration/bundle/testdata/default_python/bundle_summary.txt index 3143d729c..a0bcfdbc8 100644 --- a/integration/bundle/testdata/default_python/bundle_summary.txt +++ b/integration/bundle/testdata/default_python/bundle_summary.txt @@ -15,7 +15,8 @@ "lock": { "enabled": false } - } + }, + "uuid": "" }, "include": [ "resources/project_name_$UNIQUE_PRJ.job.yml", 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 5594749a9..ba336f6a1 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 @@ -3,6 +3,7 @@ # See https://docs.databricks.com/dev-tools/bundles/index.html for documentation. bundle: name: {{.project_name}} + uuid: {{bundle_uuid}} include: - resources/*.yml 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 c42b822a8..4d052e38e 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 @@ -2,6 +2,7 @@ # See https://docs.databricks.com/dev-tools/bundles/index.html for documentation. bundle: name: {{.project_name}} + uuid: {{bundle_uuid}} include: - resources/*.yml 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 51d03e99a..84e07df17 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 @@ -2,6 +2,7 @@ # See https://docs.databricks.com/dev-tools/bundles/index.html for documentation. bundle: name: {{.project_name}} + uuid: {{bundle_uuid}} include: - resources/*.yml From 6d3b4159bd5911c22b33c65ae245f63e06462d85 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 10 Jan 2025 09:51:59 +0100 Subject: [PATCH 48/53] Log warnings to stderr for "bundle validate -o json" (#2109) ## Changes Previously diagnostics were not seen in JSON output mode. This change prints them to stderr. This also fixes acceptance tests to preprocess all output with s/execPath/$CLI/ not just output.txt. ## Tests Existing acceptance tests. In one case I've added non-json command to check that they match in output. --- acceptance/acceptance_test.go | 13 +++++++---- .../job_tasks/out.development.stderr.txt | 6 +++++ .../bundle/override/job_tasks/output.txt | 19 +++++++++++---- acceptance/bundle/override/job_tasks/script | 3 ++- .../override/merge-string-map/output.txt | 4 ++++ cmd/bundle/validate.go | 23 +++++++++++++++---- 6 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 acceptance/bundle/override/job_tasks/out.development.stderr.txt diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 033f26dfb..b9fb219dc 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -55,12 +55,15 @@ func TestAccept(t *testing.T) { // Do not read user's ~/.databrickscfg t.Setenv(env.HomeEnvVar(), homeDir) + repls := testdiff.ReplacementsContext{} + repls.Set(execPath, "$CLI") + testDirs := getTests(t) require.NotEmpty(t, testDirs) for _, dir := range testDirs { t.Run(dir, func(t *testing.T) { t.Parallel() - runTest(t, dir) + runTest(t, dir, repls) }) } } @@ -85,7 +88,7 @@ func getTests(t *testing.T) []string { return testDirs } -func runTest(t *testing.T, dir string) { +func runTest(t *testing.T, dir string, repls testdiff.ReplacementsContext) { var tmpDir string var err error if KeepTmp { @@ -112,7 +115,7 @@ func runTest(t *testing.T, dir string) { outB, err := cmd.CombinedOutput() out := formatOutput(string(outB), err) - out = strings.ReplaceAll(out, os.Getenv("CLI"), "$CLI") + out = repls.Replace(out) doComparison(t, filepath.Join(dir, "output.txt"), "script output", out) for key := range outputs { @@ -131,7 +134,8 @@ func runTest(t *testing.T, dir string) { continue } pathExpected := filepath.Join(dir, key) - doComparison(t, pathExpected, pathNew, string(newValBytes)) + newVal := repls.Replace(string(newValBytes)) + doComparison(t, pathExpected, pathNew, newVal) } // Make sure there are not unaccounted for new files @@ -152,6 +156,7 @@ func runTest(t *testing.T, dir string) { // Show the contents & support overwrite mode for it: pathNew := filepath.Join(tmpDir, name) newVal := testutil.ReadFile(t, pathNew) + newVal = repls.Replace(newVal) doComparison(t, filepath.Join(dir, name), filepath.Join(tmpDir, name), newVal) } } diff --git a/acceptance/bundle/override/job_tasks/out.development.stderr.txt b/acceptance/bundle/override/job_tasks/out.development.stderr.txt new file mode 100644 index 000000000..7b6fef0cc --- /dev/null +++ b/acceptance/bundle/override/job_tasks/out.development.stderr.txt @@ -0,0 +1,6 @@ + +>>> errcode $CLI bundle validate -o json -t development +Error: file ./test1.py not found + + +Exit code: 1 diff --git a/acceptance/bundle/override/job_tasks/output.txt b/acceptance/bundle/override/job_tasks/output.txt index 0d561291e..0bb0b1812 100644 --- a/acceptance/bundle/override/job_tasks/output.txt +++ b/acceptance/bundle/override/job_tasks/output.txt @@ -1,8 +1,3 @@ - ->>> errcode $CLI bundle validate -o json -t development -Error: file ./test1.py not found - -Exit code: 1 { "name": "job", "queue": { @@ -36,6 +31,7 @@ Exit code: 1 >>> errcode $CLI bundle validate -o json -t staging Error: file ./test1.py not found + Exit code: 1 { "name": "job", @@ -66,3 +62,16 @@ Exit code: 1 } ] } + +>>> errcode $CLI bundle validate -t staging +Error: file ./test1.py not found + +Name: override_job_tasks +Target: staging +Workspace: + User: tester@databricks.com + Path: /Workspace/Users/tester@databricks.com/.bundle/override_job_tasks/staging + +Found 1 error + +Exit code: 1 diff --git a/acceptance/bundle/override/job_tasks/script b/acceptance/bundle/override/job_tasks/script index 4e0869857..f41729c1e 100644 --- a/acceptance/bundle/override/job_tasks/script +++ b/acceptance/bundle/override/job_tasks/script @@ -1,2 +1,3 @@ -trace errcode $CLI bundle validate -o json -t development | jq .resources.jobs.foo +trace errcode $CLI bundle validate -o json -t development 2> out.development.stderr.txt | jq .resources.jobs.foo trace errcode $CLI bundle validate -o json -t staging | jq .resources.jobs.foo +trace errcode $CLI bundle validate -t staging diff --git a/acceptance/bundle/override/merge-string-map/output.txt b/acceptance/bundle/override/merge-string-map/output.txt index e1bd7dfb4..986da8174 100644 --- a/acceptance/bundle/override/merge-string-map/output.txt +++ b/acceptance/bundle/override/merge-string-map/output.txt @@ -1,5 +1,9 @@ >>> $CLI bundle validate -o json -t dev +Warning: expected map, found string + at resources.clusters.my_cluster + in databricks.yml:6:17 + { "clusters": { "my_cluster": { diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index daeb7426d..41fa87f30 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -11,18 +11,17 @@ import ( "github.com/databricks/cli/bundle/render" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/flags" "github.com/spf13/cobra" ) -func renderJsonOutput(cmd *cobra.Command, b *bundle.Bundle, diags diag.Diagnostics) error { +func renderJsonOutput(cmd *cobra.Command, b *bundle.Bundle) error { buf, err := json.MarshalIndent(b.Config.Value().AsAny(), "", " ") if err != nil { return err } _, _ = cmd.OutOrStdout().Write(buf) - return diags.Error() + return nil } func newValidateCommand() *cobra.Command { @@ -66,7 +65,23 @@ func newValidateCommand() *cobra.Command { return nil case flags.OutputJSON: - return renderJsonOutput(cmd, b, diags) + renderOpts := render.RenderOptions{RenderSummaryTable: false} + err1 := render.RenderDiagnostics(cmd.ErrOrStderr(), b, diags, renderOpts) + err2 := renderJsonOutput(cmd, b) + + if err2 != nil { + return err2 + } + + if err1 != nil { + return err1 + } + + if diags.HasError() { + return root.ErrAlreadyPrinted + } + + return nil default: return fmt.Errorf("unknown output type %s", root.OutputType(cmd)) } From f2c4cae9f1e05aaf24bb02efa3f60df51cd7ac50 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 10 Jan 2025 10:32:39 +0100 Subject: [PATCH 49/53] Increase close-after-stale from 7 to 30 days (#2111) Giving 7 days to react before closing is too aggressive, IMO. Changed it to 30. Also changed 'stale' label from 30d to 60d. Also removed dry-run setting, it does not appear to do anything. --- .github/workflows/close-stale-issues.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml index 273b89a9c..7bf754319 100644 --- a/.github/workflows/close-stale-issues.yml +++ b/.github/workflows/close-stale-issues.yml @@ -31,10 +31,8 @@ jobs: exempt-pr-labels: No Autoclose # Issue timing - days-before-stale: 30 - days-before-close: 7 + days-before-stale: 60 + days-before-close: 30 repo-token: ${{ secrets.GITHUB_TOKEN }} loglevel: DEBUG - # TODO: Remove dry-run after merge when confirmed it works correctly - dry-run: true From 72e833a8975529b28e0c2396eb3c41c8dfb94e7c Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 10 Jan 2025 10:39:00 +0100 Subject: [PATCH 50/53] Configure dependabot to check for new github-actions (#2112) --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f1b219b47..e7d7ad6b6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,7 @@ updates: directory: "/" schedule: interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" From 75cd582021a4c0130f888f58fc7f33687d44a642 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 10 Jan 2025 10:49:33 +0100 Subject: [PATCH 51/53] Remove lint.sh; re-add 'make fmt' (#2113) See Makefile for explanation on difference between 'make fmt' and 'make lint'. I also removed lint.sh. Original motivation was to use it in aider, but it's not a good fit there, because aider passes filenames and it does not work well with most golang linters which requires whole packages to work. Follow up to #2062, #2056, #2051. --- Makefile | 10 ++++++++-- lint.sh | 14 -------------- 2 files changed, 8 insertions(+), 16 deletions(-) delete mode 100755 lint.sh diff --git a/Makefile b/Makefile index 40eef9f31..2c84d88ba 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,17 @@ PACKAGES=./acceptance/... ./libs/... ./internal/... ./cmd/... ./bundle/... . GOTESTSUM_FORMAT ?= pkgname-and-test-fails lint: - ./lint.sh ./... + golangci-lint run --fix lintcheck: golangci-lint run ./... +# Note 'make lint' will do formatting as well. However, if there are compilation errors, +# formatting/goimports will not be applied by 'make lint'. However, it will be applied by 'make fmt'. +# If you need to ensure that formatting & imports are always fixed, do "make fmt lint" +fmt: + golangci-lint run --enable-only="gofmt,gofumpt,goimports" --fix ./... + test: gotestsum --format ${GOTESTSUM_FORMAT} --no-summary=skipped -- ${PACKAGES} @@ -39,4 +45,4 @@ integration: integration-short: $(INTEGRATION) -short -.PHONY: lint lintcheck test cover showcover build snapshot vendor schema integration integration-short +.PHONY: lint lintcheck fmt test cover showcover build snapshot vendor schema integration integration-short diff --git a/lint.sh b/lint.sh deleted file mode 100755 index 1f881eaf7..000000000 --- a/lint.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -uo pipefail -# With golangci-lint, if there are any compliation issues, then formatters' autofix won't be applied. -# https://github.com/golangci/golangci-lint/issues/5257 - -golangci-lint run --fix "$@" -lint_exit_code=$? - -if [ $lint_exit_code -ne 0 ]; then - # These linters work in presence of compilation issues when run alone, so let's get these fixes at least. - golangci-lint run --enable-only="gofmt,gofumpt,goimports" --fix "$@" -fi - -exit $lint_exit_code From 99cd3fe1846d16e0b67520e390260c46badd778e Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 10 Jan 2025 10:49:57 +0100 Subject: [PATCH 52/53] Bump golangci-lint version to v1.63.4 from v1.63.1 (#2114) --- .github/workflows/push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index b71b23c4b..42245b14f 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -97,7 +97,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.63.1 + version: v1.63.4 args: --timeout=15m validate-bundle-schema: From dc3a157fdc3d800dbe0bcd7044e95eabb85275a3 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Fri, 10 Jan 2025 11:16:53 +0100 Subject: [PATCH 53/53] Remove cleanup in testcli package (#2108) ## Changes The main CLI entry point used to be a global variable, and the global state had to be cleaned up after every test run. This hasn't been the case for a while, and instead, the CLI is initialized in a function call. State accumulated by a single CLI "instance" can no longer leak into other instances, so we no longer have to perform cleanup. ## Tests Existing tests pass. --- internal/testcli/runner.go | 48 -------------------------------------- 1 file changed, 48 deletions(-) diff --git a/internal/testcli/runner.go b/internal/testcli/runner.go index 52decad2c..d32fa3947 100644 --- a/internal/testcli/runner.go +++ b/internal/testcli/runner.go @@ -6,13 +6,10 @@ import ( "context" "encoding/json" "io" - "reflect" "strings" "sync" "time" - "github.com/spf13/cobra" - "github.com/spf13/pflag" "github.com/stretchr/testify/require" "github.com/databricks/cli/cmd" @@ -68,39 +65,6 @@ func consumeLines(ctx context.Context, wg *sync.WaitGroup, r io.Reader) <-chan s return ch } -func (r *Runner) registerFlagCleanup(c *cobra.Command) { - r.Helper() - // Find target command that will be run. Example: if the command run is `databricks fs cp`, - // target command corresponds to `cp` - targetCmd, _, err := c.Find(r.args) - if err != nil && strings.HasPrefix(err.Error(), "unknown command") { - // even if command is unknown, we can proceed - require.NotNil(r, targetCmd) - } else { - require.NoError(r, err) - } - - // Force initialization of default flags. - // These are initialized by cobra at execution time and would otherwise - // not be cleaned up by the cleanup function below. - targetCmd.InitDefaultHelpFlag() - targetCmd.InitDefaultVersionFlag() - - // Restore flag values to their original value on test completion. - targetCmd.Flags().VisitAll(func(f *pflag.Flag) { - v := reflect.ValueOf(f.Value) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - // Store copy of the current flag value. - reset := reflect.New(v.Type()).Elem() - reset.Set(v) - r.Cleanup(func() { - v.Set(reset) - }) - }) -} - // Like [Runner.Eventually], but more specific func (r *Runner) WaitForTextPrinted(text string, timeout time.Duration) { r.Eventually(func() bool { @@ -159,12 +123,6 @@ func (r *Runner) RunBackground() { cli.SetIn(r.stdinR) } - // Register cleanup function to restore flags to their original values - // once test has been executed. This is needed because flag values reside - // in a global singleton data-structure, and thus subsequent tests might - // otherwise interfere with each other - r.registerFlagCleanup(cli) - errch := make(chan error) ctx, cancel := context.WithCancel(ctx) @@ -208,12 +166,6 @@ func (r *Runner) RunBackground() { } } - // Reset context on command for the next test. - // These commands are globals so we have to clean up to the best of our ability after each run. - // See https://github.com/spf13/cobra/blob/a6f198b635c4b18fff81930c40d464904e55b161/command.go#L1062-L1066 - //nolint:staticcheck // cobra sets the context and doesn't clear it - cli.SetContext(nil) - // Make caller aware of error. errch <- err close(errch)