mirror of https://github.com/databricks/cli.git
Run tests to verify backend tag validation behavior (#814)
## Changes Validation rules on tags are different per cloud (they are passed through to the underlying clusters and as such must comply with cloud-specific validation rules). This change adds tests to confirm the current behavior to ensure the normalization we can apply is in line with how the backend behaves. ## Tests The new integration tests pass (tested locally).
This commit is contained in:
parent
30b4b8ce58
commit
3685eb16f4
|
@ -0,0 +1,259 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/internal/testutil"
|
||||||
|
"github.com/databricks/databricks-sdk-go"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/compute"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testTags(t *testing.T, tags map[string]string) error {
|
||||||
|
var nodeTypeId string
|
||||||
|
switch testutil.GetCloud(t) {
|
||||||
|
case testutil.AWS:
|
||||||
|
nodeTypeId = "i3.xlarge"
|
||||||
|
case testutil.Azure:
|
||||||
|
nodeTypeId = "Standard_DS4_v2"
|
||||||
|
case testutil.GCP:
|
||||||
|
nodeTypeId = "n1-standard-4"
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := databricks.NewWorkspaceClient()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
resp, err := w.Jobs.Create(ctx, jobs.CreateJob{
|
||||||
|
Name: RandomName("test-tags-"),
|
||||||
|
Tasks: []jobs.Task{
|
||||||
|
{
|
||||||
|
TaskKey: "test",
|
||||||
|
NewCluster: &compute.ClusterSpec{
|
||||||
|
SparkVersion: "13.3.x-scala2.12",
|
||||||
|
NumWorkers: 1,
|
||||||
|
NodeTypeId: nodeTypeId,
|
||||||
|
},
|
||||||
|
SparkPythonTask: &jobs.SparkPythonTask{
|
||||||
|
PythonFile: "/doesnt_exist.py",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tags: tags,
|
||||||
|
})
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
w.Jobs.DeleteByJobId(ctx, resp.JobId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTagKey(t *testing.T, key string) error {
|
||||||
|
return testTags(t, map[string]string{
|
||||||
|
key: "value",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTagValue(t *testing.T, value string) error {
|
||||||
|
return testTags(t, map[string]string{
|
||||||
|
"key": value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type tagTestCase struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
fn func(t *testing.T, value string) error
|
||||||
|
err string
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTagTestCases(t *testing.T, cases []tagTestCase) {
|
||||||
|
for i := range cases {
|
||||||
|
tc := cases[i]
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
err := tc.fn(t, tc.value)
|
||||||
|
if tc.err == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
msg := strings.ReplaceAll(err.Error(), "\n", " ")
|
||||||
|
require.Contains(t, msg, tc.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccTagKeyAWS(t *testing.T) {
|
||||||
|
testutil.Require(t, testutil.AWS)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
runTagTestCases(t, []tagTestCase{
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
value: "café",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` The key must match the regular expression ^[\d \w\+\-=\.:\/@]*$.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode",
|
||||||
|
value: "🍎",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` contains non-latin1 characters.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
value: "",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` the minimal length is 1, and the maximum length is 127.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
value: "cafe",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ``,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccTagValueAWS(t *testing.T) {
|
||||||
|
testutil.Require(t, testutil.AWS)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
runTagTestCases(t, []tagTestCase{
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
value: "café",
|
||||||
|
fn: testTagValue,
|
||||||
|
err: ` The value must match the regular expression ^[\d \w\+\-=\.:/@]*$.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode",
|
||||||
|
value: "🍎",
|
||||||
|
fn: testTagValue,
|
||||||
|
err: ` contains non-latin1 characters.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
value: "cafe",
|
||||||
|
fn: testTagValue,
|
||||||
|
err: ``,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccTagKeyAzure(t *testing.T) {
|
||||||
|
testutil.Require(t, testutil.Azure)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
runTagTestCases(t, []tagTestCase{
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
value: "café?",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` The key must match the regular expression ^[^<>\*&%;\\\/\+\?]*$.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode",
|
||||||
|
value: "🍎",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` contains non-latin1 characters.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
value: "",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` the minimal length is 1, and the maximum length is 512.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
value: "cafe",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ``,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccTagValueAzure(t *testing.T) {
|
||||||
|
testutil.Require(t, testutil.Azure)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
runTagTestCases(t, []tagTestCase{
|
||||||
|
{
|
||||||
|
name: "unicode",
|
||||||
|
value: "🍎",
|
||||||
|
fn: testTagValue,
|
||||||
|
err: ` contains non-latin1 characters.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
value: "cafe",
|
||||||
|
fn: testTagValue,
|
||||||
|
err: ``,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccTagKeyGCP(t *testing.T) {
|
||||||
|
testutil.Require(t, testutil.GCP)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
runTagTestCases(t, []tagTestCase{
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
value: "café?",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` The key must match the regular expression ^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode",
|
||||||
|
value: "🍎",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` contains non-latin1 characters.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
value: "",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ` the minimal length is 1, and the maximum length is 63.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
value: "cafe",
|
||||||
|
fn: testTagKey,
|
||||||
|
err: ``,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccTagValueGCP(t *testing.T) {
|
||||||
|
testutil.Require(t, testutil.GCP)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
runTagTestCases(t, []tagTestCase{
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
value: "café",
|
||||||
|
fn: testTagValue,
|
||||||
|
err: ` The value must match the regular expression ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode",
|
||||||
|
value: "🍎",
|
||||||
|
fn: testTagValue,
|
||||||
|
err: ` contains non-latin1 characters.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
value: "cafe",
|
||||||
|
fn: testTagValue,
|
||||||
|
err: ``,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cloud int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AWS Cloud = iota
|
||||||
|
Azure
|
||||||
|
GCP
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implement [Requirement].
|
||||||
|
func (c Cloud) Verify(t *testing.T) {
|
||||||
|
if c != GetCloud(t) {
|
||||||
|
t.Skipf("Skipping %s-specific test", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Cloud) String() string {
|
||||||
|
switch c {
|
||||||
|
case AWS:
|
||||||
|
return "AWS"
|
||||||
|
case Azure:
|
||||||
|
return "Azure"
|
||||||
|
case GCP:
|
||||||
|
return "GCP"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCloud(t *testing.T) Cloud {
|
||||||
|
env := GetEnvOrSkipTest(t, "CLOUD_ENV")
|
||||||
|
switch env {
|
||||||
|
case "aws":
|
||||||
|
return AWS
|
||||||
|
case "azure":
|
||||||
|
return Azure
|
||||||
|
case "gcp":
|
||||||
|
return GCP
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unknown cloud environment: %s", env)
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
|
@ -35,3 +35,12 @@ func CleanupEnvironment(t *testing.T) {
|
||||||
t.Setenv("USERPROFILE", pwd)
|
t.Setenv("USERPROFILE", pwd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEnvOrSkipTest proceeds with test only with that env variable
|
||||||
|
func GetEnvOrSkipTest(t *testing.T, name string) string {
|
||||||
|
value := os.Getenv(name)
|
||||||
|
if value == "" {
|
||||||
|
t.Skipf("Environment variable %s is missing", name)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Requirement is the interface for test requirements.
|
||||||
|
type Requirement interface {
|
||||||
|
Verify(t *testing.T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require should be called at the beginning of a test to ensure that all
|
||||||
|
// requirements are met before running the test.
|
||||||
|
// If any requirement is not met, the test will be skipped.
|
||||||
|
func Require(t *testing.T, requirements ...Requirement) {
|
||||||
|
for _, r := range requirements {
|
||||||
|
r.Verify(t)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue