diff --git a/google/resource_dataproc_cluster.go b/google/resource_dataproc_cluster.go index f36ff1ca6d3..6b679411f91 100644 --- a/google/resource_dataproc_cluster.go +++ b/google/resource_dataproc_cluster.go @@ -15,6 +15,8 @@ import ( dataproc "google.golang.org/api/dataproc/v1beta2" ) +var resolveDataprocImageVersion = regexp.MustCompile(`(?P[^\s.-]+)\.(?P[^\s.-]+)(?:\.(?P[^\s.-]+))?(?:\-(?P[^\s.-]+))?`) + func resourceDataprocCluster() *schema.Resource { return &schema.Resource{ Create: resourceDataprocClusterCreate, @@ -258,10 +260,11 @@ func resourceDataprocCluster() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "image_version": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + DiffSuppressFunc: dataprocImageVersionDiffSuppress, }, "override_properties": { @@ -1005,3 +1008,51 @@ func configOptions(d *schema.ResourceData, option string) (map[string]interface{ } return nil, false } + +func dataprocImageVersionDiffSuppress(_, old, new string, _ *schema.ResourceData) bool { + oldV, err := parseDataprocImageVersion(old) + if err != nil { + return false + } + newV, err := parseDataprocImageVersion(new) + if err != nil { + return false + } + + if newV.major != oldV.major { + return false + } + if newV.minor != oldV.minor { + return false + } + // Only compare subminor version if set in config version. + if newV.subminor != "" && newV.subminor != oldV.subminor { + return false + } + // Only compare os if it is set in config version. + if newV.osName != "" && newV.osName != oldV.osName { + return false + } + return true +} + +type dataprocImageVersion struct { + major string + minor string + subminor string + osName string +} + +func parseDataprocImageVersion(version string) (*dataprocImageVersion, error) { + matches := resolveDataprocImageVersion.FindStringSubmatch(version) + if len(matches) != 5 { + return nil, fmt.Errorf("invalid image version %q", version) + } + + return &dataprocImageVersion{ + major: matches[1], + minor: matches[2], + subminor: matches[3], + osName: matches[4], + }, nil +} diff --git a/google/resource_dataproc_cluster_test.go b/google/resource_dataproc_cluster_test.go index 7cbcbbbd243..d404ccb0ac4 100644 --- a/google/resource_dataproc_cluster_test.go +++ b/google/resource_dataproc_cluster_test.go @@ -16,7 +16,7 @@ import ( "google.golang.org/api/googleapi" ) -func TestExtractInitTimeout(t *testing.T) { +func TestDataprocExtractInitTimeout(t *testing.T) { t.Parallel() actual, err := extractInitTimeout("500s") @@ -29,7 +29,7 @@ func TestExtractInitTimeout(t *testing.T) { } } -func TestExtractInitTimeout_nonSeconds(t *testing.T) { +func TestDataprocExtractInitTimeout_nonSeconds(t *testing.T) { t.Parallel() actual, err := extractInitTimeout("5m") @@ -42,7 +42,7 @@ func TestExtractInitTimeout_nonSeconds(t *testing.T) { } } -func TestExtractInitTimeout_empty(t *testing.T) { +func TestDataprocExtractInitTimeout_empty(t *testing.T) { t.Parallel() _, err := extractInitTimeout("") @@ -53,6 +53,81 @@ func TestExtractInitTimeout_empty(t *testing.T) { t.Fatalf("Expected an error with message '%s', but got %v", expected, err.Error()) } +func TestDataprocParseImageVersion(t *testing.T) { + t.Parallel() + + testCases := map[string]dataprocImageVersion{ + "1.2": {"1", "2", "", ""}, + "1.2.3": {"1", "2", "3", ""}, + "1.2.3rc": {"1", "2", "3rc", ""}, + "1.2-debian9": {"1", "2", "", "debian9"}, + "1.2.3-debian9": {"1", "2", "3", "debian9"}, + "1.2.3rc-debian9": {"1", "2", "3rc", "debian9"}, + } + + for v, expected := range testCases { + actual, err := parseDataprocImageVersion(v) + if actual.major != expected.major { + t.Errorf("parsing version %q returned error: %v", v, err) + } + if err != nil { + t.Errorf("parsing version %q returned error: %v", v, err) + } + if actual.minor != expected.minor { + t.Errorf("parsing version %q returned error: %v", v, err) + } + if actual.subminor != expected.subminor { + t.Errorf("parsing version %q returned error: %v", v, err) + } + if actual.osName != expected.osName { + t.Errorf("parsing version %q returned error: %v", v, err) + } + } + + errorTestCases := []string{ + "", + "1", + "notaversion", + "1-debian", + } + for _, v := range errorTestCases { + if _, err := parseDataprocImageVersion(v); err == nil { + t.Errorf("expected parsing invalid version %q to return error", v) + } + } +} + +func TestDataprocDiffSuppress(t *testing.T) { + t.Parallel() + + doSuppress := [][]string{ + {"1.3.10-debian9", "1.3"}, + {"1.3.10-debian9", "1.3-debian9"}, + {"1.3.10", "1.3"}, + {"1.3-debian9", "1.3"}, + } + + noSuppress := [][]string{ + {"1.3.10-debian9", "1.3.10-ubuntu"}, + {"1.3.10-debian9", "1.3.9-debian9"}, + {"1.3.10-debian9", "1.3-ubuntu"}, + {"1.3.10-debian9", "1.3.9"}, + {"1.3.10-debian9", "1.4"}, + {"1.3.10-debian9", "2.3"}, + } + + for _, tup := range doSuppress { + if !dataprocImageVersionDiffSuppress("", tup[0], tup[1], nil) { + t.Errorf("expected (old: %q, new: %q) to be suppressed", tup[0], tup[1]) + } + } + for _, tup := range noSuppress { + if dataprocImageVersionDiffSuppress("", tup[0], tup[1], nil) { + t.Errorf("expected (old: %q, new: %q) to not be suppressed", tup[0], tup[1]) + } + } +} + func TestAccDataprocCluster_missingZoneGlobalRegion1(t *testing.T) { t.Parallel()