/*
Copyright 2020 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package clusterapi

import (
	"reflect"
	"strings"
	"testing"

	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/autoscaler/cluster-autoscaler/utils/units"
)

const (
	uuid1 = "ec21c5fb-a3d5-a45f-887b-6b49aa8fc218"
	uuid2 = "ec23ebb0-bc60-443f-d139-046ec5046283"
)

func TestUtilParseScalingBounds(t *testing.T) {
	for i, tc := range []struct {
		description string
		annotations map[string]string
		error       error
		min         int
		max         int
	}{{
		description: "missing min annotation defaults to 0 and no error",
		annotations: map[string]string{
			nodeGroupMaxSizeAnnotationKey: "0",
		},
	}, {
		description: "missing max annotation defaults to 0 and no error",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "0",
		},
	}, {
		description: "invalid min errors",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "-1",
			nodeGroupMaxSizeAnnotationKey: "0",
		},
		error: errInvalidMinAnnotation,
	}, {
		description: "invalid min errors",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "not-an-int",
			nodeGroupMaxSizeAnnotationKey: "0",
		},
		error: errInvalidMinAnnotation,
	}, {
		description: "invalid max errors",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "0",
			nodeGroupMaxSizeAnnotationKey: "-1",
		},
		error: errInvalidMaxAnnotation,
	}, {
		description: "invalid max errors",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "0",
			nodeGroupMaxSizeAnnotationKey: "not-an-int",
		},
		error: errInvalidMaxAnnotation,
	}, {
		description: "negative min errors",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "-1",
			nodeGroupMaxSizeAnnotationKey: "0",
		},
		error: errInvalidMinAnnotation,
	}, {
		description: "negative max errors",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "0",
			nodeGroupMaxSizeAnnotationKey: "-1",
		},
		error: errInvalidMaxAnnotation,
	}, {
		description: "max < min errors",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "1",
			nodeGroupMaxSizeAnnotationKey: "0",
		},
		error: errInvalidMaxAnnotation,
	}, {
		description: "result is: min 0, max 0",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "0",
			nodeGroupMaxSizeAnnotationKey: "0",
		},
		min: 0,
		max: 0,
	}, {
		description: "result is min 0, max 1",
		annotations: map[string]string{
			nodeGroupMinSizeAnnotationKey: "0",
			nodeGroupMaxSizeAnnotationKey: "1",
		},
		min: 0,
		max: 1,
	}, {
		description: "deprecated min/max annotations still work, result is min 0, max 1",
		annotations: map[string]string{
			deprecatedNodeGroupMinSizeAnnotationKey: "0",
			deprecatedNodeGroupMaxSizeAnnotationKey: "1",
		},
		min: 0,
		max: 1,
	}, {
		description: "deprecated min/max annotations do not take precedence over non-deprecated annotations, result is min 1, max 2",
		annotations: map[string]string{
			deprecatedNodeGroupMinSizeAnnotationKey: "0",
			deprecatedNodeGroupMaxSizeAnnotationKey: "1",
			nodeGroupMinSizeAnnotationKey:           "1",
			nodeGroupMaxSizeAnnotationKey:           "2",
		},
		min: 1,
		max: 2,
	}} {
		t.Run(tc.description, func(t *testing.T) {
			machineSet := unstructured.Unstructured{
				Object: map[string]interface{}{
					"kind":       machineSetKind,
					"apiVersion": "cluster.x-k8s.io/v1alpha3",
					"metadata": map[string]interface{}{
						"name":      "test",
						"namespace": "default",
					},
					"spec":   map[string]interface{}{},
					"status": map[string]interface{}{},
				},
			}
			machineSet.SetAnnotations(tc.annotations)

			min, max, err := parseScalingBounds(machineSet.GetAnnotations())
			if tc.error != nil && err == nil {
				t.Fatalf("test #%d: expected an error", i)
			}

			if tc.error != nil && tc.error != err {
				if !strings.HasPrefix(err.Error(), tc.error.Error()) {
					t.Errorf("expected message to have prefix %q, got %q", tc.error.Error(), err)
				}
			}

			if tc.error == nil {
				if tc.min != min {
					t.Errorf("expected min %d, got %d", tc.min, min)
				}
				if tc.max != max {
					t.Errorf("expected max %d, got %d", tc.max, max)
				}
			}
		})
	}
}

func TestUtilGetOwnerByKindMachineSet(t *testing.T) {
	for _, tc := range []struct {
		description         string
		machineSet          *unstructured.Unstructured
		machineSetOwnerRefs []metav1.OwnerReference
		expectedOwnerRef    *metav1.OwnerReference
	}{{
		description:         "not owned as no owner references",
		machineSet:          &unstructured.Unstructured{},
		machineSetOwnerRefs: []metav1.OwnerReference{},
		expectedOwnerRef:    nil,
	}, {
		description: "not owned as not the same Kind",
		machineSet: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "test",
					"namespace": "default",
				},
				"spec":   map[string]interface{}{},
				"status": map[string]interface{}{},
			},
		},
		machineSetOwnerRefs: []metav1.OwnerReference{
			{
				Kind: "Other",
			},
		},
		expectedOwnerRef: nil,
	}, {
		description: "not owned because no OwnerReference.Name",
		machineSet: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "test",
					"namespace": "default",
				},
				"spec":   map[string]interface{}{},
				"status": map[string]interface{}{},
			},
		},
		machineSetOwnerRefs: []metav1.OwnerReference{
			{
				Kind: machineDeploymentKind,
				UID:  uuid1,
			},
		},
		expectedOwnerRef: nil,
	}, {
		description: "owned as UID values match and same Kind and Name not empty",
		machineSet: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "test",
					"namespace": "default",
				},
				"spec":   map[string]interface{}{},
				"status": map[string]interface{}{},
			},
		},
		machineSetOwnerRefs: []metav1.OwnerReference{
			{
				Kind: machineDeploymentKind,
				Name: "foo",
				UID:  uuid1,
			},
		},
		expectedOwnerRef: &metav1.OwnerReference{
			Kind: machineDeploymentKind,
			Name: "foo",
			UID:  uuid1,
		},
	}} {
		t.Run(tc.description, func(t *testing.T) {
			tc.machineSet.SetOwnerReferences(tc.machineSetOwnerRefs)

			ownerRef := getOwnerForKind(tc.machineSet, machineDeploymentKind)
			if !reflect.DeepEqual(tc.expectedOwnerRef, ownerRef) {
				t.Errorf("expected %v, got %v", tc.expectedOwnerRef, ownerRef)
			}
		})
	}
}

func TestUtilGetOwnerByKindMachine(t *testing.T) {
	for _, tc := range []struct {
		description      string
		machine          *unstructured.Unstructured
		machineOwnerRefs []metav1.OwnerReference
		expectedOwnerRef *metav1.OwnerReference
	}{{
		description:      "not owned as no owner references",
		machine:          &unstructured.Unstructured{},
		machineOwnerRefs: []metav1.OwnerReference{},
		expectedOwnerRef: nil,
	}, {
		description: "not owned as not the same Kind",
		machine: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "test",
					"namespace": "default",
				},
				"spec":   map[string]interface{}{},
				"status": map[string]interface{}{},
			},
		},
		machineOwnerRefs: []metav1.OwnerReference{
			{
				Kind: "Other",
				Name: "foo",
				UID:  uuid1,
			},
		},
		expectedOwnerRef: nil,
	}, {
		description: "not owned because no OwnerReference.Name",
		machine: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "test",
					"namespace": "default",
				},
				"spec":   map[string]interface{}{},
				"status": map[string]interface{}{},
			},
		},
		machineOwnerRefs: []metav1.OwnerReference{
			{
				Kind: machineSetKind,
				UID:  uuid1,
			},
		},
		expectedOwnerRef: nil,
	}, {
		description: "owned as UID values match and same Kind and Name not empty",
		machine: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "test",
					"namespace": "default",
				},
				"spec":   map[string]interface{}{},
				"status": map[string]interface{}{},
			},
		},
		machineOwnerRefs: []metav1.OwnerReference{
			{
				Kind: machineSetKind,
				Name: "foo",
				UID:  uuid2,
			},
		},
		expectedOwnerRef: &metav1.OwnerReference{
			Kind: machineSetKind,
			Name: "foo",
			UID:  uuid2,
		},
	}} {
		t.Run(tc.description, func(t *testing.T) {
			tc.machine.SetOwnerReferences(tc.machineOwnerRefs)

			ownerRef := getOwnerForKind(tc.machine, machineSetKind)
			if !reflect.DeepEqual(tc.expectedOwnerRef, ownerRef) {
				t.Errorf("expected %v, got %v", tc.expectedOwnerRef, ownerRef)
			}
		})
	}
}

func TestUtilMachineSetHasMachineDeploymentOwnerRef(t *testing.T) {
	for _, tc := range []struct {
		description         string
		machineSet          *unstructured.Unstructured
		machineSetOwnerRefs []metav1.OwnerReference
		owned               bool
	}{{
		description:         "machineset not owned as no owner references",
		machineSet:          &unstructured.Unstructured{},
		machineSetOwnerRefs: []metav1.OwnerReference{},
		owned:               false,
	}, {
		description: "machineset not owned as ownerref not a MachineDeployment",
		machineSet: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "test",
					"namespace": "default",
				},
				"spec":   map[string]interface{}{},
				"status": map[string]interface{}{},
			},
		},
		machineSetOwnerRefs: []metav1.OwnerReference{
			{
				Kind: "Other",
			},
		},
		owned: false,
	}, {
		description: "machineset owned as Kind matches and Name not empty",
		machineSet: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "test",
					"namespace": "default",
				},
				"spec":   map[string]interface{}{},
				"status": map[string]interface{}{},
			},
		},
		machineSetOwnerRefs: []metav1.OwnerReference{
			{
				Kind: machineDeploymentKind,
				Name: "foo",
			},
		},
		owned: true,
	}} {
		t.Run(tc.description, func(t *testing.T) {
			tc.machineSet.SetOwnerReferences(tc.machineSetOwnerRefs)
			owned := machineSetHasMachineDeploymentOwnerRef(tc.machineSet)
			if tc.owned != owned {
				t.Errorf("expected %t, got %t", tc.owned, owned)
			}
		})
	}
}

func TestUtilNormalizedProviderID(t *testing.T) {
	for _, tc := range []struct {
		description string
		providerID  string
		expectedID  normalizedProviderID
	}{{
		description: "nil string yields empty string",
		providerID:  "",
		expectedID:  "",
	}, {
		description: "empty string",
		providerID:  "",
		expectedID:  "",
	}, {
		description: "id without / characters",
		providerID:  "i-12345678",
		expectedID:  "i-12345678",
	}, {
		description: "id with / characters",
		providerID:  "aws:////i-12345678",
		expectedID:  "i-12345678",
	}} {
		t.Run(tc.description, func(t *testing.T) {
			actualID := normalizedProviderString(tc.providerID)
			if actualID != tc.expectedID {
				t.Errorf("expected %v, got %v", tc.expectedID, actualID)
			}
		})
	}
}

func TestScaleFromZeroEnabled(t *testing.T) {
	for _, tc := range []struct {
		description string
		enabled     bool
		annotations map[string]string
	}{{
		description: "nil annotations",
		enabled:     false,
	}, {
		description: "empty annotations",
		annotations: map[string]string{},
		enabled:     false,
	}, {
		description: "non-matching annotation",
		annotations: map[string]string{"foo": "bar"},
		enabled:     false,
	}, {
		description: "matching key, incomplete annotations",
		annotations: map[string]string{
			"foo":  "bar",
			cpuKey: "1",
			gpuKey: "2",
		},
		enabled: false,
	}, {
		description: "matching key, complete annotations",
		annotations: map[string]string{
			"foo":     "bar",
			cpuKey:    "1",
			memoryKey: "2",
		},
		enabled: true,
	}} {
		t.Run(tc.description, func(t *testing.T) {
			got := scaleFromZeroEnabled(tc.annotations)
			if tc.enabled != got {
				t.Errorf("expected %t, got %t", tc.enabled, got)
			}
		})
	}
}

func TestParseCPUCapacity(t *testing.T) {
	for _, tc := range []struct {
		description      string
		annotations      map[string]string
		expectedQuantity resource.Quantity
		expectedError    bool
	}{{
		description:      "nil annotations",
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    false,
	}, {
		description:      "empty annotations",
		annotations:      map[string]string{},
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    false,
	}, {
		description:      "bad quantity",
		annotations:      map[string]string{cpuKey: "not-a-quantity"},
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    true,
	}, {
		description:      "valid quantity with units",
		annotations:      map[string]string{cpuKey: "123m"},
		expectedError:    false,
		expectedQuantity: resource.MustParse("123m"),
	}, {
		description:      "valid quantity without units",
		annotations:      map[string]string{cpuKey: "1"},
		expectedError:    false,
		expectedQuantity: resource.MustParse("1"),
	}, {
		description:      "valid fractional quantity without units",
		annotations:      map[string]string{cpuKey: "0.1"},
		expectedError:    false,
		expectedQuantity: resource.MustParse("0.1"),
	}} {
		t.Run(tc.description, func(t *testing.T) {
			got, err := parseCPUCapacity(tc.annotations)
			if tc.expectedError && err == nil {
				t.Fatal("expected an error")
			}
			if tc.expectedQuantity.Cmp(got) != 0 {
				t.Errorf("expected %v, got %v", tc.expectedQuantity.String(), got.String())
			}
		})
	}
}

func TestParseMemoryCapacity(t *testing.T) {
	for _, tc := range []struct {
		description      string
		annotations      map[string]string
		expectedQuantity resource.Quantity
		expectedError    bool
	}{{
		description:      "nil annotations",
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    false,
	}, {
		description:      "empty annotations",
		annotations:      map[string]string{},
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    false,
	}, {
		description:      "bad quantity",
		annotations:      map[string]string{memoryKey: "not-a-quantity"},
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    true,
	}, {
		description:      "valid quantity",
		annotations:      map[string]string{memoryKey: "456"},
		expectedError:    false,
		expectedQuantity: *resource.NewQuantity(456*units.MiB, resource.DecimalSI),
	}, {
		description:      "quantity with unit type (Mi)",
		annotations:      map[string]string{memoryKey: "456Mi"},
		expectedError:    true,
		expectedQuantity: zeroQuantity.DeepCopy(),
	}, {
		description:      "quantity with unit type (Gi)",
		annotations:      map[string]string{memoryKey: "8Gi"},
		expectedError:    true,
		expectedQuantity: zeroQuantity.DeepCopy(),
	}} {
		t.Run(tc.description, func(t *testing.T) {
			got, err := parseMemoryCapacity(tc.annotations)
			if tc.expectedError && err == nil {
				t.Fatal("expected an error")
			}
			if tc.expectedQuantity.Cmp(got) != 0 {
				t.Errorf("expected %v, got %v", tc.expectedQuantity.String(), got.String())
			}
		})
	}
}

func TestParseGPUCapacity(t *testing.T) {
	for _, tc := range []struct {
		description      string
		annotations      map[string]string
		expectedQuantity resource.Quantity
		expectedError    bool
	}{{
		description:      "nil annotations",
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    false,
	}, {
		description:      "empty annotations",
		annotations:      map[string]string{},
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    false,
	}, {
		description:      "bad quantity",
		annotations:      map[string]string{gpuKey: "not-a-quantity"},
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    true,
	}, {
		description:      "valid quantity",
		annotations:      map[string]string{gpuKey: "13"},
		expectedError:    false,
		expectedQuantity: resource.MustParse("13"),
	}} {
		t.Run(tc.description, func(t *testing.T) {
			got, err := parseGPUCapacity(tc.annotations)
			if tc.expectedError && err == nil {
				t.Fatal("expected an error")
			}
			if tc.expectedQuantity.Cmp(got) != 0 {
				t.Errorf("expected %v, got %v", tc.expectedQuantity.String(), got.String())
			}
		})
	}
}

func TestParseMaxPodsCapacity(t *testing.T) {
	for _, tc := range []struct {
		description      string
		annotations      map[string]string
		expectedQuantity resource.Quantity
		expectedError    bool
	}{{
		description:      "nil annotations",
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    false,
	}, {
		description:      "empty annotations",
		annotations:      map[string]string{},
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    false,
	}, {
		description:      "bad quantity",
		annotations:      map[string]string{maxPodsKey: "not-a-quantity"},
		expectedQuantity: zeroQuantity.DeepCopy(),
		expectedError:    true,
	}, {
		description:      "valid quantity",
		annotations:      map[string]string{maxPodsKey: "13"},
		expectedError:    false,
		expectedQuantity: resource.MustParse("13"),
	}} {
		t.Run(tc.description, func(t *testing.T) {
			got, err := parseMaxPodsCapacity(tc.annotations)
			if tc.expectedError && err == nil {
				t.Fatal("expected an error")
			}
			if tc.expectedQuantity.Cmp(got) != 0 {
				t.Errorf("expected %v, got %v", tc.expectedQuantity.String(), got.String())
			}
		})
	}
}

func Test_clusterNameFromResource(t *testing.T) {
	for _, tc := range []struct {
		name     string
		resource *unstructured.Unstructured
		want     string
	}{{
		name: "cluster name not set, v1alpha1 MachineSet",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.k8s.io/v1alpha1",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "",
	}, {
		name: "cluster name not set, v1alpha1 MachineDeployment",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineDeploymentKind,
				"apiVersion": "cluster.k8s.io/v1alpha1",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "",
	}, {
		name: "cluster name set in MachineSet labels, v1alpha1 MachineSet",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.k8s.io/v1alpha1",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
					"labels": map[string]interface{}{
						deprecatedClusterNameLabel: "bar",
					},
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "bar",
	}, {
		name: "cluster name set in MachineDeployment, v1alpha1 MachineDeployment",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineDeploymentKind,
				"apiVersion": "cluster.k8s.io/v1alpha1",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
					"labels": map[string]interface{}{
						deprecatedClusterNameLabel: "bar",
					},
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "bar",
	}, {
		name: "cluster name set in Machine template labels, v1alpha1 MachineSet",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.k8s.io/v1alpha1",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
					"template": map[string]interface{}{
						"metadata": map[string]interface{}{
							"labels": map[string]interface{}{
								deprecatedClusterNameLabel: "bar",
							},
						},
					},
				},
				"status": map[string]interface{}{},
			},
		},
		want: "bar",
	}, {
		name: "cluster name set in Machine template, v1alpha1 MachineDeployment",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineDeploymentKind,
				"apiVersion": "cluster.k8s.io/v1alpha1",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
					"template": map[string]interface{}{
						"metadata": map[string]interface{}{
							"labels": map[string]interface{}{
								deprecatedClusterNameLabel: "bar",
							},
						},
					},
				},
				"status": map[string]interface{}{},
			},
		},
		want: "bar",
	}, {
		name: "cluster name not set, v1alpha2 MachineSet",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha2",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "",
	}, {
		name: "cluster name not set, v1alpha2 MachineDeployment",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineDeploymentKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha2",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "",
	}, {
		name: "cluster name set in MachineSet labels, v1alpha2 MachineSet",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha2",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
					"labels": map[string]interface{}{
						clusterNameLabel: "bar",
					},
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "bar",
	}, {
		name: "cluster name set in MachineDeployment, v1alpha2 MachineDeployment",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineDeploymentKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha2",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
					"labels": map[string]interface{}{
						clusterNameLabel: "bar",
					},
				},
				"spec": map[string]interface{}{
					"replicas": int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "bar",
	}, {
		name: "cluster name set in spec, v1alpha3 MachineSet",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineSetKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
				},
				"spec": map[string]interface{}{
					"clusterName": "bar",
					"replicas":    int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "bar",
	}, {
		name: "cluster name set in spec, v1alpha3 MachineDeployment",
		resource: &unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind":       machineDeploymentKind,
				"apiVersion": "cluster.x-k8s.io/v1alpha3",
				"metadata": map[string]interface{}{
					"name":      "foo",
					"namespace": "default",
				},
				"spec": map[string]interface{}{
					"clusterName": "bar",
					"replicas":    int64(1),
				},
				"status": map[string]interface{}{},
			},
		},
		want: "bar",
	}} {
		t.Run(tc.name, func(t *testing.T) {
			if got := clusterNameFromResource(tc.resource); got != tc.want {
				t.Errorf("clusterNameFromResource() = %v, want %v", got, tc.want)
			}
		})
	}
}
