autoscaler/cluster-autoscaler/cloudprovider/gce/gce_manager_test.go

1582 lines
58 KiB
Go

/*
Copyright 2017 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 gce
import (
"fmt"
"regexp"
"strings"
"testing"
"time"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
"k8s.io/autoscaler/cluster-autoscaler/config"
. "k8s.io/autoscaler/cluster-autoscaler/utils/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
gce "google.golang.org/api/compute/v1"
)
const (
projectId = "project1"
zoneB = "us-central1-b"
zoneC = "us-central1-c"
zoneF = "us-central1-f"
region = "us-central1"
defaultPoolMigName = "gke-cluster-1-default-pool"
defaultPool = "default-pool"
extraPoolMigName = "gke-cluster-1-extra-pool-323233232"
extraPool2MigName = "gke-cluster-1-extra-pool2-323233232"
extraPool = "extra-pool"
clusterName = "cluster1"
gceMigA = "gce-mig-a"
gceMigB = "gce-mig-b"
)
const instanceGroupManagerResponseTemplate = `{
"kind": "compute#instanceGroupManager",
"id": "3213213219",
"creationTimestamp": "2017-09-15T04:47:24.687-07:00",
"name": "%s",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s",
"instanceTemplate": "https://www.googleapis.com/compute/v1/projects/project1/global/instanceTemplates/%s",
"instanceGroup": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups/%s",
"baseInstanceName": "%s",
"fingerprint": "kfdsuH",
"currentActions": {
"none": 3,
"creating": 0,
"creatingWithoutRetries": 0,
"recreating": 0,
"deleting": 0,
"abandoning": 0,
"restarting": 0,
"refreshing": 0
},
"targetSize": %v,
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroupManagers/%s"
}
`
const instanceGroupManagerTargetSize4ResponseTemplate = `{
"kind": "compute#instanceGroupManager",
"id": "3213213219",
"creationTimestamp": "2017-09-15T04:47:24.687-07:00",
"name": "%s",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s",
"instanceTemplate": "https://www.googleapis.com/compute/v1/projects/project1/global/instanceTemplates/%s",
"instanceGroup": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups/%s",
"baseInstanceName": "gke-cluster-1-default-pool-f23aac-grp",
"fingerprint": "kfdsuH",
"currentActions": {
"none": 3,
"creating": 0,
"creatingWithoutRetries": 0,
"recreating": 0,
"deleting": 0,
"abandoning": 0,
"restarting": 0,
"refreshing": 0
},
"targetSize": 4,
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroupManagers/%s"
}
`
const instanceTemplate = `
{
"kind": "compute#instanceTemplate",
"id": "28701103232323232",
"creationTimestamp": "2017-09-15T04:47:21.577-07:00",
"name": "gke-cluster-1-default-pool",
"description": "",
"properties": {
"tags": {
"items": [
"gke-cluster-1-fc0afeeb-node"
]
},
"machineType": "n1-standard-1",
"canIpForward": true,
"networkInterfaces": [
{
"kind": "compute#networkInterface",
"network": "https://www.googleapis.com/compute/v1/projects/project1/global/networks/default",
"subnetwork": "https://www.googleapis.com/compute/v1/projects/project1/regions/us-central1/subnetworks/default",
"accessConfigs": [
{
"kind": "compute#accessConfig",
"type": "ONE_TO_ONE_NAT",
"name": "external-nat"
}
]
}
],
"disks": [
{
"kind": "compute#attachedDisk",
"type": "PERSISTENT",
"mode": "READ_WRITE",
"boot": true,
"initializeParams": {
"sourceImage": "https://www.googleapis.com/compute/v1/projects/gke-node-images/global/images/cos-stable-60-9592-84-0",
"diskSizeGb": "100",
"diskType": "pd-standard"
},
"autoDelete": true
}
],
"metadata": {
"kind": "compute#metadata",
"fingerprint": "F7n_RsHD3ng=",
"items": [
{
"key": "kube-env",
"value": "ALLOCATE_NODE_CIDRS: \"true\"\n"
},
{
"key": "user-data",
"value": "#cloud-config\n\nwrite_files:\n - path: /etc/systemd/system/kube-node-installation.service\n "
},
{
"key": "gci-update-strategy",
"value": "update_disabled"
},
{
"key": "gci-ensure-gke-docker",
"value": "true"
},
{
"key": "configure-sh",
"value": "#!/bin/bash\n\n# Copyright 2016 The Kubernetes Authors.\n#\n# Licensed under the Apache License, "
},
{
"key": "cluster-name",
"value": "cluster-1"
}
]
},
"serviceAccounts": [
{
"email": "default",
"scopes": [
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring.write",
"https://www.googleapis.com/auth/servicecontrol",
"https://www.googleapis.com/auth/service.management.readonly",
"https://www.googleapis.com/auth/trace.append"
]
}
],
"scheduling": {
"onHostMaintenance": "MIGRATE",
"automaticRestart": true,
"preemptible": false
}
},
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/global/instanceTemplates/gke-cluster-1-default-pool-f7607aac"
}`
const fourRunningInstancesManagedInstancesResponseTemplate = `{
"managedInstances": [
{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s-f7607aac-9j4g",
"id": "1974815549671473983",
"instanceStatus": "RUNNING",
"currentAction": "NONE"
},
{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s-f7607aac-c63g",
"currentAction": "RUNNING",
"id": "197481554967143333",
"instanceStatus": "RUNNING",
"currentAction": "NONE"
},
{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s-f7607aac-dck1",
"id": "4462422841867240255",
"instanceStatus": "RUNNING",
"currentAction": "NONE"
},
{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s-f7607aac-f1hm",
"id": "6309299611401323327",
"instanceStatus": "RUNNING",
"currentAction": "NONE"
}
]
}`
const oneRunningInstanceManagedInstancesResponseTemplate = `{
"managedInstances": [
{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s-gdf607aac-9j4g",
"id": "1974815323221473983",
"instanceStatus": "RUNNING",
"currentAction": "NONE"
}
]
}`
const listInstanceGroupManagerResponsePartTemplate = `
{
"kind": "compute#instanceGroupManager",
"id": "9012769713544464023",
"creationTimestamp": "2019-03-26T07:34:32.082-07:00",
"name": "%v",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/%v",
"instanceTemplate": "https://www.googleapis.com/compute/v1/projects/project1/global/instanceTemplates/gke-blah-default-pool-67b773a0",
"versions": [
{
"instanceTemplate": "https://www.googleapis.com/compute/v1/projects/project1/global/instanceTemplates/gke-blah-default-pool-67b773a0",
"targetSize": {
"calculated": 1
}
}
],
"instanceGroup": "https://www.googleapis.com/compute/v1/projects/lukaszos-gke-dev2/zones/%v/instanceGroups/%v",
"baseInstanceName": "%s",
"fingerprint": "ASJwTpesjDI=",
"currentActions": {
"none": 1,
"creating": 0,
"creatingWithoutRetries": 0,
"verifying": 0,
"recreating": 0,
"deleting": 0,
"abandoning": 0,
"restarting": 0,
"refreshing": 0
},
"status": {
"isStable": true
},
"targetSize": %v,
"selfLink": "https://www.googleapis.com/compute/v1/projects/lukaszos-gke-dev2/zones/us-west1-b/instanceGroupManagers/gke-blah-default-pool-67b773a0-grp",
"updatePolicy": {
"type": "OPPORTUNISTIC",
"minimalAction": "REPLACE",
"maxSurge": {
"fixed": 1,
"calculated": 1
},
"maxUnavailable": {
"fixed": 1,
"calculated": 1
}
}
}
`
func buildDefaultInstanceGroupManagerResponse(zone string) string {
return buildInstanceGroupManagerResponse(zone, defaultPoolMigName, 3)
}
func buildInstanceGroupManagerResponse(zone string, instanceGroup string, targetSize uint64) string {
return fmt.Sprintf(instanceGroupManagerResponseTemplate, instanceGroup, zone, instanceGroup, zone, instanceGroup, instanceGroup, targetSize, zone, instanceGroup)
}
func buildFourRunningInstancesOnDefaultMigManagedInstancesResponse(zone string) string {
return buildFourRunningInstancesManagedInstancesResponse(zone, defaultPoolMigName)
}
func buildFourRunningInstancesManagedInstancesResponse(zone string, instanceGroup string) string {
return fmt.Sprintf(fourRunningInstancesManagedInstancesResponseTemplate, zone, instanceGroup, zone, instanceGroup, zone, instanceGroup, zone, instanceGroup)
}
func buildOneRunningInstanceOnExtraPoolMigManagedInstancesResponse(zone string) string {
return buildOneRunningInstanceManagedInstancesResponse(zone, extraPoolMigName)
}
func buildOneRunningInstanceManagedInstancesResponse(zone string, instanceGroup string) string {
return fmt.Sprintf(oneRunningInstanceManagedInstancesResponseTemplate, zone, instanceGroup)
}
func buildListInstanceGroupManagersResponsePart(name, zone string, targetSize uint64) string {
return fmt.Sprintf(listInstanceGroupManagerResponsePartTemplate, name, zone, zone, name, name, targetSize)
}
func buildListInstanceGroupManagersResponse(listInstanceGroupManagerResponseParts ...string) string {
return `{
"kind": "compute#instanceGroupManagerList",
"id": "blah",
"items": [` +
strings.Join(listInstanceGroupManagerResponseParts, ",") +
`], "selfLink": "https://blah"}`
}
func newTestGceManager(t *testing.T, testServerURL string, regional bool) *gceManagerImpl {
gceService := newTestAutoscalingGceClient(t, projectId, testServerURL, "")
// Override wait for op timeouts.
gceService.operationWaitTimeout = 50 * time.Millisecond
gceService.operationPollInterval = 1 * time.Millisecond
cache := &GceCache{
migs: make(map[GceRef]Mig),
instances: make(map[GceRef][]cloudprovider.Instance),
instancesToMig: make(map[GceRef]GceRef),
instancesFromUnknownMig: make(map[GceRef]bool),
autoscalingOptionsCache: map[GceRef]map[string]string{},
machinesCache: map[MachineTypeKey]MachineType{
{"us-central1-b", "n1-standard-1"}: {Name: "n1-standard-1", CPU: 1, Memory: 1},
{"us-central1-c", "n1-standard-1"}: {Name: "n1-standard-1", CPU: 1, Memory: 1},
{"us-central1-f", "n1-standard-1"}: {Name: "n1-standard-1", CPU: 1, Memory: 1},
},
migTargetSizeCache: map[GceRef]int64{},
instanceTemplateNameCache: map[GceRef]string{},
instanceTemplatesCache: map[GceRef]*gce.InstanceTemplate{},
migBaseNameCache: map[GceRef]string{},
}
migLister := NewMigLister(cache)
manager := &gceManagerImpl{
cache: cache,
migLister: migLister,
migInfoProvider: NewCachingMigInfoProvider(cache, migLister, gceService, projectId, 1),
GceService: gceService,
projectId: projectId,
regional: regional,
templates: &GceTemplateBuilder{},
explicitlyConfigured: make(map[GceRef]bool),
concurrentGceRefreshes: 1,
}
if regional {
manager.location = region
} else {
manager.location = zoneB
}
return manager
}
const deleteInstancesResponse = `{
"kind": "compute#operation",
"id": "8554136016090105726",
"name": "operation-1505802641136-55984ff86d980-a99e8c2b-0c8aaaaa",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a",
"operationType": "compute.instanceGroupManagers.deleteInstances",
"targetLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/instanceGroupManagers/gke-cluster-1-default-pool-f7607aac-grp",
"targetId": "5382990249302819619",
"status": "DONE",
"user": "user@example.com",
"progress": 100,
"insertTime": "2017-09-18T23:30:41.612-07:00",
"startTime": "2017-09-18T23:30:41.618-07:00",
"endTime": "2017-09-18T23:30:41.618-07:00",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/operations/operation-1505802641136-55984ff86d980-a99e8c2b-0c8aaaaa"
}`
const deleteInstancesOperationResponse = `
{
"kind": "compute#operation",
"id": "8554136016090105726",
"name": "operation-1505802641136-55984ff86d980-a99e8c2b-0c8aaaaa",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a",
"operationType": "compute.instanceGroupManagers.deleteInstances",
"targetLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/instanceGroupManagers/gke-cluster-1-default-pool-f7607aac-grp",
"targetId": "5382990249302819619",
"status": "DONE",
"user": "user@example.com",
"progress": 100,
"insertTime": "2017-09-18T23:30:41.612-07:00",
"startTime": "2017-09-18T23:30:41.618-07:00",
"endTime": "2017-09-18T23:30:41.618-07:00",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/operations/operation-1505802641136-55984ff86d980-a99e8c2b-0c8aaaaa"
}`
func setupTestDefaultPool(manager *gceManagerImpl, setupBaseName bool) *gceMig {
mig := &gceMig{
gceRef: GceRef{
Name: defaultPoolMigName,
Zone: zoneB,
Project: projectId,
},
gceManager: manager,
minSize: 1,
maxSize: 11,
}
manager.cache.migs[mig.GceRef()] = mig
if setupBaseName {
manager.cache.migBaseNameCache[mig.GceRef()] = defaultPoolMigName
}
return mig
}
func setupTestExtraPool(manager *gceManagerImpl, setupBaseName bool) *gceMig {
mig := &gceMig{
gceRef: GceRef{
Name: extraPoolMigName,
Zone: zoneB,
Project: projectId,
},
gceManager: manager,
minSize: 0,
maxSize: 1000,
}
manager.cache.migs[mig.GceRef()] = mig
if setupBaseName {
manager.cache.migBaseNameCache[mig.GceRef()] = extraPoolMigName
}
return mig
}
func setupTestExtraPool2(manager *gceManagerImpl, setupBaseName bool) *gceMig {
mig := &gceMig{
gceRef: GceRef{
Name: extraPool2MigName,
Zone: zoneC,
Project: projectId,
},
gceManager: manager,
minSize: 0,
maxSize: 1000,
}
manager.cache.migs[mig.GceRef()] = mig
if setupBaseName {
manager.cache.migBaseNameCache[mig.GceRef()] = extraPool2MigName
}
return mig
}
func TestDeleteInstances(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
g := newTestGceManager(t, server.URL, false)
setupTestDefaultPool(g, false)
setupTestExtraPool(g, true)
// Get basename for defaultPool
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers/gke-cluster-1-default-pool").Return(buildDefaultInstanceGroupManagerResponse(zoneB)).Once()
// Regenerate instances for defaultPool
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers/gke-cluster-1-default-pool/listManagedInstances").Return(buildFourRunningInstancesOnDefaultMigManagedInstancesResponse(zoneB)).Once()
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers/gke-cluster-1-default-pool/deleteInstances").Return(deleteInstancesResponse).Once()
server.On("handle", "/projects/project1/zones/us-central1-b/operations/operation-1505802641136-55984ff86d980-a99e8c2b-0c8aaaaa").Return(deleteInstancesOperationResponse).Once()
instances := []GceRef{
{
Project: projectId,
Zone: zoneB,
Name: "gke-cluster-1-default-pool-f7607aac-f1hm",
},
{
Project: projectId,
Zone: zoneB,
Name: "gke-cluster-1-default-pool-f7607aac-c63g",
},
}
err := g.DeleteInstances(instances)
assert.NoError(t, err)
mock.AssertExpectationsForObjects(t, server)
// Regenerate instances for extraPool (no basename call because it is already in cache)
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers/gke-cluster-1-extra-pool-323233232/listManagedInstances").Return(buildOneRunningInstanceOnExtraPoolMigManagedInstancesResponse(zoneB)).Once()
// Fail on deleting instances from different MIGs.
instances = []GceRef{
{
Project: projectId,
Zone: zoneB,
Name: "gke-cluster-1-default-pool-f7607aac-f1hm",
},
{
Project: projectId,
Zone: zoneB,
Name: "gke-cluster-1-extra-pool-323233232-gdf607aac-9j4g",
},
}
err = g.DeleteInstances(instances)
assert.Error(t, err)
assert.Equal(t, "cannot delete instances which don't belong to the same MIG.", err.Error())
mock.AssertExpectationsForObjects(t, server)
}
// TODO; make Test*MigSize tests use MigTargetSizesProvider mock and move mocking API server to tests of cachingMigTargetSizesProvider
const setMigSizeResponse = `{
"kind": "compute#operation",
"id": "7558996788000226430",
"name": "operation-1505739408819-5597646964339-eb839c88-28805931",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a",
"operationType": "compute.instanceGroupManagers.resize",
"targetLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/instanceGroupManagers/gke-cluster-1-default-pool-f7607aac-grp",
"targetId": "5382990249302819619",
"status": "DONE",
"user": "user@example.com",
"progress": 100,
"insertTime": "2017-09-18T05:56:49.227-07:00",
"startTime": "2017-09-18T05:56:49.230-07:00",
"endTime": "2017-09-18T05:56:49.230-07:00",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/operations/operation-1505739408819-5597646964339-eb839c88-28805931"
}`
const setMigSizeOperationResponse = `{
"kind": "compute#operation",
"id": "7558996788000226430",
"name": "operation-1505739408819-5597646964339-eb839c88-28805931",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a",
"operationType": "compute.instanceGroupManagers.resize",
"targetLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/instanceGroupManagers/gke-cluster-1-default-pool-f7607aac-grp",
"targetId": "5382990249302819619",
"status": "DONE",
"user": "user@example.com",
"progress": 100,
"insertTime": "2017-09-18T05:56:49.227-07:00",
"startTime": "2017-09-18T05:56:49.230-07:00",
"endTime": "2017-09-18T05:56:49.230-07:00",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/operations/operation-1505739408819-5597646964339-eb839c88-28805931"
}`
func TestGetAndSetMigSize(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
g := newTestGceManager(t, server.URL, false)
extraPoolMig := setupTestExtraPool(g, true)
defaultPoolMig := setupTestDefaultPool(g, true)
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers").Return(
buildListInstanceGroupManagersResponse(
buildListInstanceGroupManagersResponsePart(defaultPoolMigName, zoneB, 7),
buildListInstanceGroupManagersResponsePart(extraPoolMigName, zoneB, 8),
)).Once()
// getting size for defaultPoolMig should trigger listing all the InstanceGroupManagers
defaultPoolMigSize, err := g.GetMigSize(defaultPoolMig)
assert.NoError(t, err)
assert.Equal(t, int64(7), defaultPoolMigSize)
mock.AssertExpectationsForObjects(t, server)
// extra queries for defaultPoolMig and extraPoolMig should not result in any extra API calls
defaultPoolMigSize, err = g.GetMigSize(defaultPoolMig)
assert.NoError(t, err)
assert.Equal(t, int64(7), defaultPoolMigSize)
extraPoolMigSize, err := g.GetMigSize(extraPoolMig)
assert.NoError(t, err)
assert.Equal(t, int64(8), extraPoolMigSize)
mock.AssertExpectationsForObjects(t, server)
// set target size for extraPoolMig; will require resize API call and API call for polling for resize operation
server.On("handle", fmt.Sprintf("/projects/project1/zones/us-central1-b/instanceGroupManagers/%s/resize", extraPoolMigName)).Return(setMigSizeResponse).Once()
server.On("handle", "/projects/project1/zones/us-central1-b/operations/operation-1505739408819-5597646964339-eb839c88-28805931").Return(setMigSizeOperationResponse).Once()
err = g.SetMigSize(extraPoolMig, 4)
assert.NoError(t, err)
mock.AssertExpectationsForObjects(t, server)
// query for size of resized extraPoolMig; no extra API calls
extraPoolMigSize, err = g.GetMigSize(extraPoolMig)
assert.NoError(t, err)
assert.Equal(t, int64(4), extraPoolMigSize)
mock.AssertExpectationsForObjects(t, server)
// register another pool: extraPool2; pool uses mig in different zone
extraPool2Mig := setupTestExtraPool2(g, true)
// query for size of resized extraPool2Mig; execting API call refreshing target sizes
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers").Return(
buildListInstanceGroupManagersResponse(
buildListInstanceGroupManagersResponsePart(defaultPoolMigName, zoneB, 7),
buildListInstanceGroupManagersResponsePart(extraPoolMigName, zoneB, 8),
)).Once()
server.On("handle", "/projects/project1/zones/us-central1-c/instanceGroupManagers").Return(
buildListInstanceGroupManagersResponse(
buildListInstanceGroupManagersResponsePart(extraPool2MigName, zoneC, 9)),
).Once()
extraPool2MigSize, err := g.GetMigSize(extraPool2Mig)
assert.NoError(t, err)
assert.Equal(t, int64(9), extraPool2MigSize)
mock.AssertExpectationsForObjects(t, server)
// another query for size of extraPool2Mig will not result in any API calls
extraPool2MigSize, err = g.GetMigSize(extraPool2Mig)
assert.NoError(t, err)
assert.Equal(t, int64(9), extraPool2MigSize)
mock.AssertExpectationsForObjects(t, server)
// let's invalidate target size cache
// TODO we should probably call g.Refresh here but that imples more API calls. Leaving just partial cache invalidation for now
g.cache.InvalidateAllMigTargetSizes()
// now if w query size of any mig whole cache should be refreshed by listing InstanceGroupManagers; we expect two calls
// for zoneB and zoneC
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers").Return(
buildListInstanceGroupManagersResponse(
buildListInstanceGroupManagersResponsePart(defaultPoolMigName, zoneB, 7),
buildListInstanceGroupManagersResponsePart(extraPoolMigName, zoneB, 8),
)).Once()
server.On("handle", "/projects/project1/zones/us-central1-c/instanceGroupManagers").Return(
buildListInstanceGroupManagersResponse(
buildListInstanceGroupManagersResponsePart(extraPool2MigName, zoneC, 9),
)).Once()
extraPool2MigSize, err = g.GetMigSize(extraPool2Mig)
assert.NoError(t, err)
assert.Equal(t, int64(9), extraPool2MigSize)
mock.AssertExpectationsForObjects(t, server)
}
func TestGetMigSizeListCallFails(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
g := newTestGceManager(t, server.URL, false)
extraPoolMig := setupTestExtraPool(g, true)
defaultPoolMig := setupTestDefaultPool(g, true)
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers").Return("bad_response").Once()
server.On("handle", fmt.Sprintf("/projects/project1/zones/us-central1-b/instanceGroupManagers/%s", defaultPoolMigName)).Return(buildInstanceGroupManagerResponse(zoneB, defaultPoolMigName, 7)).Once()
// getting size for defaultPoolMig should trigger listing all the InstanceGroupManagers
defaultPoolMigSize, err := g.GetMigSize(defaultPoolMig)
assert.NoError(t, err)
assert.Equal(t, int64(7), defaultPoolMigSize)
mock.AssertExpectationsForObjects(t, server)
// extra queries for defaultPoolMig and extraPoolMig should not result in any extra API calls
defaultPoolMigSize, err = g.GetMigSize(defaultPoolMig)
assert.NoError(t, err)
assert.Equal(t, int64(7), defaultPoolMigSize)
// Querying another mig will yet again try to list all migs
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers").Return(
buildListInstanceGroupManagersResponse(
buildListInstanceGroupManagersResponsePart(defaultPoolMigName, zoneB, 7),
buildListInstanceGroupManagersResponsePart(extraPoolMigName, zoneB, 8),
)).Once()
// getting size for defaultPoolMig should trigger listing all the InstanceGroupManagers
extraPoolMigSize, err := g.GetMigSize(extraPoolMig)
assert.NoError(t, err)
assert.Equal(t, int64(8), extraPoolMigSize)
mock.AssertExpectationsForObjects(t, server)
}
func TestGetMigForInstance(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
g := newTestGceManager(t, server.URL, false)
setupTestDefaultPool(g, false)
g.cache.InvalidateAllMigBasenames()
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers").Return(
buildListInstanceGroupManagersResponse(
buildListInstanceGroupManagersResponsePart(defaultPoolMigName, zoneB, 7),
buildListInstanceGroupManagersResponsePart(extraPoolMigName, zoneB, 8),
)).Once()
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers/gke-cluster-1-default-pool/listManagedInstances").Return(buildFourRunningInstancesOnDefaultMigManagedInstancesResponse(zoneB)).Twice()
gceRef1 := GceRef{
Project: projectId,
Zone: zoneB,
Name: "gke-cluster-1-default-pool-f7607aac-f1hm",
}
mig, err := g.GetMigForInstance(gceRef1)
assert.NoError(t, err)
assert.NotNil(t, mig)
assert.Equal(t, "gke-cluster-1-default-pool", mig.GceRef().Name)
gceRef2 := GceRef{
Project: projectId,
Zone: zoneB,
Name: "gke-cluster-1-default-pool-f7607aac-0000", // instance from unknown MIG
}
mig, err = g.GetMigForInstance(gceRef2)
assert.NoError(t, err)
assert.Nil(t, mig)
_, found := g.cache.instancesFromUnknownMig[gceRef2]
assert.True(t, found)
mock.AssertExpectationsForObjects(t, server)
}
func TestGetMigNodesBasic(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
g := newTestGceManager(t, server.URL, false)
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers/extra-pool-323233232/listManagedInstances").Return(buildFourRunningInstancesOnDefaultMigManagedInstancesResponse(zoneB)).Once()
mig := &gceMig{
gceRef: GceRef{
Project: projectId,
Zone: zoneB,
Name: "extra-pool-323233232",
},
gceManager: g,
minSize: 0,
maxSize: 1000,
}
nodes, err := g.GetMigNodes(mig)
assert.NoError(t, err)
assert.Equal(t, 4, len(nodes))
assert.Equal(t, "gce://project1/us-central1-b/gke-cluster-1-default-pool-f7607aac-9j4g", nodes[0].Id)
assert.Equal(t, "gce://project1/us-central1-b/gke-cluster-1-default-pool-f7607aac-c63g", nodes[1].Id)
assert.Equal(t, "gce://project1/us-central1-b/gke-cluster-1-default-pool-f7607aac-dck1", nodes[2].Id)
assert.Equal(t, "gce://project1/us-central1-b/gke-cluster-1-default-pool-f7607aac-f1hm", nodes[3].Id)
for i := 0; i < 4; i++ {
assert.Nil(t, nodes[i].Status.ErrorInfo)
assert.Equal(t, cloudprovider.InstanceRunning, nodes[i].Status.State)
}
}
const managedInstancesResponseTemplate = `{"managedInstances": [%s]}`
const managedInstanceWithInstanceStatusResponsePartTemplate = `{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s",
"id": "1776565833558018907",
"instanceStatus": "%s",
"version": {
"instanceTemplate": "https://www.googleapis.com/compute/beta/projects/project1/global/instanceTemplates/test-1-cpu-1-k80-2"
},
"currentAction": "NONE"
}
`
const managedInstanceWithInstanceStatusAndCurrentActionResponsePartTemplate = `{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s",
"instanceStatus": "%s",
"version": {
"instanceTemplate": "https://www.googleapis.com/compute/beta/projects/project1/global/instanceTemplates/test-1-cpu-1-k80-2"
},
"currentAction": "%s",
"lastAttempt": {}
}
`
const managedInstanceWithInstanceStatusAndWithCurrentActionAndErrorResponsePartTemplate = `{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s",
"instanceStatus": "%s",
"version": {
"instanceTemplate": "https://www.googleapis.com/compute/beta/projects/project1/global/instanceTemplates/test-1-cpu-1-k80-2"
},
"currentAction": "%s",
"lastAttempt": {
"errors": {
"errors": [
{
"code": "%s",
"message": "%s"
}
]
}
}
}
`
const managedInstanceWithCurrentActionResponsePartTemplate = `{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s",
"version": {
"instanceTemplate": "https://www.googleapis.com/compute/beta/projects/project1/global/instanceTemplates/test-1-cpu-1-k80-2"
},
"currentAction": "%s",
"lastAttempt": {}
}
`
const managedInstanceWithCurrentActionAndErrorResponsePartTemplate = `{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s",
"version": {
"instanceTemplate": "https://www.googleapis.com/compute/beta/projects/project1/global/instanceTemplates/test-1-cpu-1-k80-2"
},
"currentAction": "%s",
"lastAttempt": {
"errors": {
"errors": [
{
"code": "%s",
"message": "%s"
}
]
}
}
}
`
const managedInstanceWithCurrentActionAndTwoErrorsResponsePartTemplate = `{
"instance": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instances/%s",
"version": {
"instanceTemplate": "https://www.googleapis.com/compute/beta/projects/project1/global/instanceTemplates/test-1-cpu-1-k80-2"
},
"currentAction": "%s",
"lastAttempt": {
"errors": {
"errors": [
{
"code": "%s",
"message": "%s"
},
{
"code": "%s",
"message": "%s"
}
]
}
}
}
`
func buildManagedInstancesResponse(managedInstanceParts ...string) string {
partsString := ""
for _, part := range managedInstanceParts {
if partsString != "" {
partsString += ", "
}
partsString += part
}
return fmt.Sprintf(managedInstancesResponseTemplate, partsString)
}
func buildRunningManagedInstanceResponsePart(zone string, instanceName string) string {
return buildManagedInstanceWithInstanceStatusResponsePart(zone, instanceName, "RUNNING")
}
func buildRunningManagedInstanceWithCurrentActionResponsePart(zone string, instanceName string, currentAction string) string {
return buildManagedInstanceWithInstanceStatusAndCurrentActionResponsePart(zone, instanceName, "RUNNING", currentAction)
}
func buildRunningManagedInstanceWithCurrentActionAndErrorResponsePart(zone string, instanceName string, currentAction string, code string, message string) string {
return buildManagedInstanceWithInstanceStatusAndCurrentActionAndErrorResponsePart(zone, instanceName, "RUNNING", currentAction, code, message)
}
func buildManagedInstanceWithInstanceStatusResponsePart(zone string, instanceName string, instanceStatus string) string {
return fmt.Sprintf(managedInstanceWithInstanceStatusResponsePartTemplate, zone, instanceName, instanceStatus)
}
func buildManagedInstanceWithInstanceStatusAndCurrentActionResponsePart(zone string, instanceName string, instanceStatus string, currentAction string) string {
return fmt.Sprintf(managedInstanceWithInstanceStatusAndCurrentActionResponsePartTemplate, zone, instanceName, instanceStatus, currentAction)
}
func buildManagedInstanceWithInstanceStatusAndCurrentActionAndErrorResponsePart(zone string, instanceName string, instanceStatus string, currentAction string, code string, message string) string {
return fmt.Sprintf(managedInstanceWithInstanceStatusAndWithCurrentActionAndErrorResponsePartTemplate, zone, instanceName, instanceStatus, currentAction, code, message)
}
func buildManagedInstanceWithCurrentActionResponsePart(zone string, instanceName string, currentAction string) string {
return fmt.Sprintf(managedInstanceWithCurrentActionResponsePartTemplate, zone, instanceName, currentAction)
}
func buildManagedInstanceWithCurrentActionAndErrorResponsePart(zone string, instanceName string, currentAction string, code string, message string) string {
return fmt.Sprintf(managedInstanceWithCurrentActionAndErrorResponsePartTemplate, zone, instanceName, currentAction, code, message)
}
func buildManagedInstanceWithCurrentActionAndTwoErrorsResponsePart(zone string, instanceName string, currentAction string, code1 string, message1 string, code2 string, message2 string) string {
return fmt.Sprintf(managedInstanceWithCurrentActionAndTwoErrorsResponsePartTemplate, zone, instanceName, currentAction, code1, message1, code2, message2)
}
func TestGetMigNodesComplex(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
g := newTestGceManager(t, server.URL, false)
testCases := []struct {
instanceName string
responsePart string
expectedState cloudprovider.InstanceState
expectedErrorClass cloudprovider.InstanceErrorClass
expectedErrorCode string
expectedErrorMessage string
}{
{
"running-creating-no_error",
buildRunningManagedInstanceResponsePart("europe-west1-b", "running-creating-no_error"),
cloudprovider.InstanceRunning,
0,
"",
"",
},
{
"none-creating-quota_exceeded",
buildManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "none-creating-quota_exceeded", "CREATING", ErrorCodeQuotaExceeded, "We run out of quota while creating!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeQuotaExceeded,
"We run out of quota while creating!",
},
{
"none-recreating-quota_exceeded",
buildManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "none-recreating-quota_exceeded", "RECREATING", ErrorCodeQuotaExceeded, "We run out of quota while recreating!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeQuotaExceeded,
"We run out of quota while recreating!",
},
{
"none-creating_no_retries-quota_exceeded",
buildManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "none-creating_no_retries-quota_exceeded", "CREATING_WITHOUT_RETRIES", ErrorCodeQuotaExceeded, "We run out of quota while creating without retries!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeQuotaExceeded,
"We run out of quota while creating without retries!",
},
{
"none-creating-other_error",
buildManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "none-creating-other_error", "CREATING", "SOME_ERROR", "Ojojojoj!"),
cloudprovider.InstanceCreating,
cloudprovider.OtherErrorClass,
ErrorCodeOther,
"Ojojojoj!",
},
{
"none-creating-other_error_and_quota_exceeded",
buildManagedInstanceWithCurrentActionAndTwoErrorsResponsePart("europe-west1-b", "none-creating-other_error_and_quota_exceeded", "CREATING", "SOME_ERROR", "Ojojojoj!", ErrorCodeQuotaExceeded, "We run out of quota!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeQuotaExceeded,
"Ojojojoj!; We run out of quota!",
},
{
"none-creating-quota_exceeded_and_other_error",
buildManagedInstanceWithCurrentActionAndTwoErrorsResponsePart("europe-west1-b", "none-creating-quota_exceeded_and_other_error", "CREATING", ErrorCodeQuotaExceeded, "We run out of quota!", "SOME_ERROR", "Ojojojoj!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeQuotaExceeded,
"We run out of quota!; Ojojojoj!",
},
{
"none-deleting-no_error",
buildManagedInstanceWithCurrentActionResponsePart("europe-west1-b", "none-deleting-no_error", "DELETING"),
cloudprovider.InstanceDeleting,
0,
"",
"",
},
{
"running-deleting-no_error",
buildRunningManagedInstanceWithCurrentActionResponsePart("europe-west1-b", "running-deleting-no_error", "DELETING"),
cloudprovider.InstanceDeleting,
0,
"",
"",
},
{
"running-deleting-other_error",
buildRunningManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "running-deleting-other_error", "DELETING", "SOME_ERROR", "Error while deleting"),
cloudprovider.InstanceDeleting,
0,
"",
"",
},
{
"none-creating-resource_pool_exhausted_error",
buildManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "none-creating-resource_pool_exhausted_error", "CREATING", "RESOURCE_POOL_EXHAUSTED", "No resources!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeResourcePoolExhausted,
"No resources!",
},
{
"none-creating-zonal_resource_pool_exhausted_error",
buildManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "none-creating-zonal_resource_pool_exhausted_error", "CREATING", "ZONE_RESOURCE_POOL_EXHAUSTED", "No resources!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeResourcePoolExhausted,
"No resources!",
},
{
"none-creating-zonal_resource_pool_exhausted_error_with_details",
buildManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "none-creating-zonal_resource_pool_exhausted_error_with_details", "CREATING", "ZONE_RESOURCE_POOL_EXHAUSTED_WITH_DETAILS", "No resources!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeResourcePoolExhausted,
"No resources!",
},
{
"running-creating-resource_pool_exhausted_error",
buildRunningManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "running-creating-resource_pool_exhausted_error", "CREATING", "RESOURCE_POOL_EXHAUSTED", "No resources!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeResourcePoolExhausted,
"No resources!",
},
{
"running-creating-quota_error",
buildRunningManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "running-creating-quota_error", "CREATING", ErrorCodeQuotaExceeded, "We run out of quota while creating!"),
cloudprovider.InstanceCreating,
cloudprovider.OutOfResourcesErrorClass,
ErrorCodeQuotaExceeded,
"We run out of quota while creating!",
},
{
"running-creating-other_error",
buildRunningManagedInstanceWithCurrentActionAndErrorResponsePart("europe-west1-b", "running-creating-other_error", "CREATING", "SOME_ERROR", "Ojojojoj!"),
cloudprovider.InstanceCreating,
0,
"",
"",
},
{
"repairing-creating-other_error",
buildManagedInstanceWithInstanceStatusAndCurrentActionAndErrorResponsePart("europe-west1-b", "repairing-creating-other_error", "REPAIRING", "CREATING", "SOME_ERROR", "Ojojojoj!"),
cloudprovider.InstanceCreating,
0,
"",
"",
},
{
"provisioning-creating-other_error",
buildManagedInstanceWithInstanceStatusAndCurrentActionAndErrorResponsePart("europe-west1-b", "provisioning-creating-other_error", "PROVISIONING", "CREATING", "SOME_ERROR", "Ojojojoj!"),
cloudprovider.InstanceCreating,
cloudprovider.OtherErrorClass,
ErrorCodeOther,
"Ojojojoj!",
},
{
"staging-creating-other_error",
buildManagedInstanceWithInstanceStatusAndCurrentActionAndErrorResponsePart("europe-west1-b", "staging-creating-other_error", "STAGING", "CREATING", "SOME_ERROR", "Ojojojoj!"),
cloudprovider.InstanceCreating,
cloudprovider.OtherErrorClass,
ErrorCodeOther,
"Ojojojoj!",
},
}
parts := make([]string, 0)
for _, tc := range testCases {
parts = append(parts, tc.responsePart)
}
response := buildManagedInstancesResponse(parts...)
server.On("handle", "/projects/project1/zones/europe-west1-b/instanceGroupManagers/some_group/listManagedInstances").Return(response).Once()
mig := &gceMig{
gceRef: GceRef{
Project: projectId,
Zone: "europe-west1-b",
Name: "some_group",
},
gceManager: g,
minSize: 0,
maxSize: 1000,
}
nodes, err := g.GetMigNodes(mig)
assert.NoError(t, err)
assert.Equal(t, len(testCases), len(nodes))
for i, tc := range testCases {
instanceInfo := nodes[i]
assert.Equal(t, fmt.Sprintf("gce://project1/europe-west1-b/%s", tc.instanceName), instanceInfo.Id)
assert.Equal(t, tc.expectedState, instanceInfo.Status.State)
if tc.expectedErrorClass == 0 {
assert.Nil(t, instanceInfo.Status.ErrorInfo)
} else {
assert.NotNil(t, instanceInfo.Status.ErrorInfo)
assert.Equal(t, tc.expectedErrorClass, instanceInfo.Status.ErrorInfo.ErrorClass)
assert.Equal(t, tc.expectedErrorCode, instanceInfo.Status.ErrorInfo.ErrorCode)
assert.Equal(t, tc.expectedErrorMessage, instanceInfo.Status.ErrorInfo.ErrorMessage)
}
}
mock.AssertExpectationsForObjects(t, server)
}
const instanceGroupResponsePartTemplate = `{
"kind": "compute#instanceGroup",
"id": "1121230570947910218",
"name": "%s",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups/%s",
"size": 1
}`
func buildInstanceGroupResponsePart(zone string, instanceGroup string) string {
return fmt.Sprintf(instanceGroupResponsePartTemplate, instanceGroup, zone, instanceGroup)
}
const listInstanceGroupsResponseTemplate = `{
"kind": "compute#instanceGroupList",
"id": "projects/project1a/zones/%s/instanceGroups",
"items": [%s],
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups"
}`
func buildListInstanceGroupsResponse(zone string, instanceGroups ...string) string {
var items []string
for _, instanceGroup := range instanceGroups {
items = append(items, buildInstanceGroupResponsePart(zone, instanceGroup))
}
return fmt.Sprintf(listInstanceGroupsResponseTemplate,
zone,
strings.Join(items, ", "),
zone,
)
}
const getRegionResponse = `{
"kind": "compute#region",
"id": "1000",
"creationTimestamp": "1969-12-31T16:00:00.000-08:00",
"name": "us-central1",
"description": "us-central1",
"status": "UP",
"zones": [
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b"
],
"quotas": [],
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/regions/us-central1"
}`
func TestFetchAutoMigsZonal(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroups").Return(buildListInstanceGroupsResponse(zoneB, gceMigA, gceMigB)).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigA).Return(buildInstanceGroupManagerResponse(zoneB, gceMigA, 3)).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigB).Return(buildInstanceGroupManagerResponse(zoneB, gceMigB, 3)).Once()
// Regenerate instance cache
server.On("handle", "/projects/project1/global/instanceTemplates/"+gceMigA).Return(instanceTemplate).Once()
server.On("handle", "/projects/project1/global/instanceTemplates/"+gceMigB).Return(instanceTemplate).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigA+"/listManagedInstances").Return(buildFourRunningInstancesManagedInstancesResponse(zoneB, gceMigA)).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigB+"/listManagedInstances").Return(buildOneRunningInstanceManagedInstancesResponse(zoneB, gceMigB)).Once()
regional := false
g := newTestGceManager(t, server.URL, regional)
min, max := 0, 100
g.migAutoDiscoverySpecs = []migAutoDiscoveryConfig{
{Re: regexp.MustCompile("UNUSED"), MinSize: min, MaxSize: max},
}
assert.NoError(t, g.fetchAutoMigs())
migs := g.GetMigs()
assert.Equal(t, 2, len(migs))
validateMigExists(t, migs, zoneB, gceMigA, min, max)
validateMigExists(t, migs, zoneB, gceMigB, min, max)
mock.AssertExpectationsForObjects(t, server)
}
func TestFetchAutoMigsUnregistersMissingMigs(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
// Register explicit instance group
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigA).Return(buildInstanceGroupManagerResponse(zoneB, gceMigA, 3)).Once()
server.On("handle", "/projects/project1/global/instanceTemplates/"+gceMigA).Return(instanceTemplate).Once()
// Regenerate cache for explicit instance group
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigA+"/listManagedInstances").Return(buildFourRunningInstancesManagedInstancesResponse(zoneB, gceMigA)).Twice()
// Register 'previously autodetected' instance group
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigB).Return(buildInstanceGroupManagerResponse(zoneB, gceMigB, 3)).Once()
server.On("handle", "/projects/project1/global/instanceTemplates/"+gceMigB).Return(instanceTemplate).Once()
regional := false
g := newTestGceManager(t, server.URL, regional)
// This MIG should never be unregistered because it is explicitly configured.
minA, maxA := 0, 100
specs := []string{fmt.Sprintf("%d:%d:https://content.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups/%s", minA, maxA, zoneB, gceMigA)}
assert.NoError(t, g.fetchExplicitMigs(specs))
// This MIG was previously autodetected but is now gone.
// It should be unregistered.
unregister := &gceMig{
gceManager: g,
gceRef: GceRef{Project: projectId, Zone: zoneB, Name: gceMigB},
minSize: 1,
maxSize: 10,
}
assert.True(t, g.registerMig(unregister))
assert.NoError(t, g.fetchAutoMigs())
migs := g.GetMigs()
assert.Equal(t, 1, len(migs))
validateMigExists(t, migs, zoneB, gceMigA, minA, maxA)
mock.AssertExpectationsForObjects(t, server)
}
func TestFetchAutoMigsRegional(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
server.On("handle", "/projects/project1/regions/us-central1").Return(getRegionResponse).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroups").Return(buildListInstanceGroupsResponse(zoneB, gceMigA, gceMigB)).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigA).Return(buildInstanceGroupManagerResponse(zoneB, gceMigA, 3)).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigB).Return(buildInstanceGroupManagerResponse(zoneB, gceMigB, 3)).Once()
// Regenerate instance cache
server.On("handle", "/projects/project1/global/instanceTemplates/"+gceMigA).Return(instanceTemplate).Once()
server.On("handle", "/projects/project1/global/instanceTemplates/"+gceMigB).Return(instanceTemplate).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigA+"/listManagedInstances").Return(buildFourRunningInstancesManagedInstancesResponse(zoneB, gceMigA)).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigB+"/listManagedInstances").Return(buildOneRunningInstanceManagedInstancesResponse(zoneB, gceMigB)).Once()
regional := true
g := newTestGceManager(t, server.URL, regional)
min, max := 0, 100
g.migAutoDiscoverySpecs = []migAutoDiscoveryConfig{
{Re: regexp.MustCompile("UNUSED"), MinSize: min, MaxSize: max},
}
assert.NoError(t, g.fetchAutoMigs())
migs := g.GetMigs()
assert.Equal(t, 2, len(migs))
validateMigExists(t, migs, zoneB, gceMigA, min, max)
validateMigExists(t, migs, zoneB, gceMigB, min, max)
mock.AssertExpectationsForObjects(t, server)
}
func TestFetchExplicitMigs(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigA).Return(buildInstanceGroupManagerResponse(zoneB, gceMigA, 3)).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigB).Return(buildInstanceGroupManagerResponse(zoneB, gceMigB, 3)).Once()
server.On("handle", "/projects/project1/global/instanceTemplates/"+gceMigA).Return(instanceTemplate).Once()
server.On("handle", "/projects/project1/global/instanceTemplates/"+gceMigB).Return(instanceTemplate).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigA+"/listManagedInstances").Return(buildFourRunningInstancesManagedInstancesResponse(zoneB, gceMigA)).Once()
server.On("handle", "/projects/project1/zones/"+zoneB+"/instanceGroupManagers/"+gceMigB+"/listManagedInstances").Return(buildOneRunningInstanceManagedInstancesResponse(zoneB, gceMigB)).Once()
regional := false
g := newTestGceManager(t, server.URL, regional)
minA, maxA := 0, 100
minB, maxB := 1, 10
specs := []string{
fmt.Sprintf("%d:%d:https://content.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups/%s", minA, maxA, zoneB, gceMigA),
fmt.Sprintf("%d:%d:https://content.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups/%s", minB, maxB, zoneB, gceMigB),
}
assert.NoError(t, g.fetchExplicitMigs(specs))
migs := g.GetMigs()
assert.Equal(t, 2, len(migs))
validateMigExists(t, migs, zoneB, gceMigA, minA, maxA)
validateMigExists(t, migs, zoneB, gceMigB, minB, maxB)
mock.AssertExpectationsForObjects(t, server)
}
const listMachineTypesResponse = `{
"kind": "compute#machineTypeList",
"id": "projects/project1/zones/us-central1-c/machineTypes",
"items": [
{
"kind": "compute#machineType",
"id": "1000",
"creationTimestamp": "1969-12-31T16:00:00.000-08:00",
"name": "f1-micro",
"description": "1 vCPU (shared physical core) and 0.6 GB RAM",
"guestCpus": 1,
"memoryMb": 614,
"imageSpaceGb": 0,
"maximumPersistentDisks": 16,
"maximumPersistentDisksSizeGb": "3072",
"zone": "us-central1-c",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-c/machineTypes/f1-micro",
"isSharedCpu": true
},
{
"kind": "compute#machineType",
"id": "2000",
"creationTimestamp": "1969-12-31T16:00:00.000-08:00",
"name": "g1-small",
"description": "1 vCPU (shared physical core) and 1.7 GB RAM",
"guestCpus": 1,
"memoryMb": 1740,
"imageSpaceGb": 0,
"maximumPersistentDisks": 16,
"maximumPersistentDisksSizeGb": "3072",
"zone": "us-central1-c",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-c/machineTypes/g1-small",
"isSharedCpu": true
}
],
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-c/machineTypes"
}`
func TestGetMigTemplateNode(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers/default-pool").Return(getInstanceGroupManagerResponse).Once()
server.On("handle", "/projects/project1/global/instanceTemplates/gke-cluster-1-default-pool").Return(instanceTemplate).Once()
regional := false
g := newTestGceManager(t, server.URL, regional)
mig := &gceMig{
gceRef: GceRef{
Project: projectId,
Zone: zoneB,
Name: "default-pool",
},
gceManager: g,
minSize: 0,
maxSize: 1000,
}
node, err := g.GetMigTemplateNode(mig)
assert.NoError(t, err)
assert.NotNil(t, node)
mock.AssertExpectationsForObjects(t, server)
}
func validateMigExists(t *testing.T, migs []Mig, zone string, name string, minSize int, maxSize int) {
ref := GceRef{
projectId,
zone,
name,
}
for _, mig := range migs {
if mig.GceRef() == ref {
assert.Equal(t, minSize, mig.MinSize())
assert.Equal(t, maxSize, mig.MaxSize())
return
}
}
allRefs := []GceRef{}
for _, mig := range migs {
allRefs = append(allRefs, mig.GceRef())
}
assert.Failf(t, "Mig not found", "Mig %v not found among %v", ref, allRefs)
}
func TestParseMIGAutoDiscoverySpecs(t *testing.T) {
cases := []struct {
name string
specs []string
want []migAutoDiscoveryConfig
wantErr bool
}{
{
name: "GoodSpecs",
specs: []string{
"mig:namePrefix=pfx,min=0,max=10",
"mig:namePrefix=anotherpfx,min=1,max=2",
},
want: []migAutoDiscoveryConfig{
{Re: regexp.MustCompile("^pfx.+"), MinSize: 0, MaxSize: 10},
{Re: regexp.MustCompile("^anotherpfx.+"), MinSize: 1, MaxSize: 2},
},
},
{
name: "MissingMIGType",
specs: []string{"namePrefix=pfx,min=0,max=10"},
wantErr: true,
},
{
name: "WrongType",
specs: []string{"asg:namePrefix=pfx,min=0,max=10"},
wantErr: true,
},
{
name: "UnknownKey",
specs: []string{"mig:namePrefix=pfx,min=0,max=10,unknown=hi"},
wantErr: true,
},
{
name: "NonIntegerMin",
specs: []string{"mig:namePrefix=pfx,min=a,max=10"},
wantErr: true,
},
{
name: "NonIntegerMax",
specs: []string{"mig:namePrefix=pfx,min=1,max=donkey"},
wantErr: true,
},
{
name: "PrefixDoesNotCompileToRegexp",
specs: []string{"mig:namePrefix=a),min=1,max=10"},
wantErr: true,
},
{
name: "KeyMissingValue",
specs: []string{"mig:namePrefix=prefix,min=,max=10"},
wantErr: true,
},
{
name: "ValueMissingKey",
specs: []string{"mig:namePrefix=prefix,=0,max=10"},
wantErr: true,
},
{
name: "KeyMissingSeparator",
specs: []string{"mig:namePrefix=prefix,min,max=10"},
wantErr: true,
},
{
name: "TooManySeparators",
specs: []string{"mig:namePrefix=prefix,min=0,max=10=20"},
wantErr: true,
},
{
name: "PrefixIsEmpty",
specs: []string{"mig:namePrefix=,min=0,max=10"},
wantErr: true,
},
{
name: "PrefixIsMissing",
specs: []string{"mig:min=0,max=10"},
wantErr: true,
},
{
name: "MaxBelowMin",
specs: []string{"mig:namePrefix=prefix,min=10,max=1"},
wantErr: true,
},
{
name: "MaxIsZero",
specs: []string{"mig:namePrefix=prefix,min=0,max=0"},
wantErr: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
do := cloudprovider.NodeGroupDiscoveryOptions{NodeGroupAutoDiscoverySpecs: tc.specs}
got, err := parseMIGAutoDiscoverySpecs(do)
if tc.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.True(t, assert.ObjectsAreEqualValues(tc.want, got), "\ngot: %#v\nwant: %#v", got, tc.want)
})
}
}
const createInstancesResponse = `{
"kind": "compute#operation",
"id": "2890052495600280364",
"name": "operation-1624366531120-5c55a4e128c15-fc5daa90-e1ef6c32",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b",
"operationType": "compute.instanceGroupManagers.createInstances",
"targetLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroupManagers/gke-cluster-1-default-pool-e25725dc-grp",
"targetId": "7836594831806456968",
"status": "DONE",
"user": "user@example.com",
"progress": 100,
"insertTime": "2021-06-22T05:55:31.903-07:00",
"startTime": "2021-06-22T05:55:31.907-07:00",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/operations/operation-1624366531120-5c55a4e128c15-fc5daa90-e1ef6c32"
}`
const createInstancesOperationResponse = `{
"kind": "compute#operation",
"id": "2890052495600280364",
"name": "operation-1624366531120-5c55a4e128c15-fc5daa90-e1ef6c32",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b",
"operationType": "compute.instanceGroupManagers.createInstances",
"targetLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroupManagers/gke-cluster-1-default-pool-e25725dc-grp",
"targetId": "7836594831806456968",
"status": "DONE",
"user": "user@example.com",
"progress": 100,
"insertTime": "2021-06-22T05:55:31.903-07:00",
"startTime": "2021-06-22T05:55:31.907-07:00",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/operations/operation-1624366531120-5c55a4e128c15-fc5daa90-e1ef6c32"
}`
func TestAppendInstances(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
g := newTestGceManager(t, server.URL, false)
defaultPoolMig := setupTestDefaultPool(g, true)
server.On("handle", "/projects/project1/zones/us-central1-b/instanceGroupManagers/gke-cluster-1-default-pool/listManagedInstances").Return(buildFourRunningInstancesOnDefaultMigManagedInstancesResponse(zoneB)).Once()
server.On("handle", fmt.Sprintf("/projects/project1/zones/us-central1-b/instanceGroupManagers/%v/createInstances", defaultPoolMig.gceRef.Name)).Return(createInstancesResponse).Once()
server.On("handle", "/projects/project1/zones/us-central1-b/operations/operation-1624366531120-5c55a4e128c15-fc5daa90-e1ef6c32").Return(createInstancesOperationResponse).Once()
err := g.CreateInstances(defaultPoolMig, 2)
assert.NoError(t, err)
mock.AssertExpectationsForObjects(t, server)
}
func TestGetMigOptions(t *testing.T) {
defaultOptions := &config.NodeGroupAutoscalingOptions{
ScaleDownUtilizationThreshold: 0.1,
ScaleDownGpuUtilizationThreshold: 0.2,
ScaleDownUnneededTime: time.Second,
ScaleDownUnreadyTime: time.Minute,
}
cases := []struct {
desc string
opts map[string]string
expected *config.NodeGroupAutoscalingOptions
}{
{
desc: "return provided defaults on empty metadata",
opts: map[string]string{},
expected: defaultOptions,
},
{
desc: "return specified options",
opts: map[string]string{
config.DefaultScaleDownGpuUtilizationThresholdKey: "0.6",
config.DefaultScaleDownUtilizationThresholdKey: "0.7",
config.DefaultScaleDownUnneededTimeKey: "1h",
config.DefaultScaleDownUnreadyTimeKey: "30m",
},
expected: &config.NodeGroupAutoscalingOptions{
ScaleDownGpuUtilizationThreshold: 0.6,
ScaleDownUtilizationThreshold: 0.7,
ScaleDownUnneededTime: time.Hour,
ScaleDownUnreadyTime: 30 * time.Minute,
},
},
{
desc: "complete partial options specs with defaults",
opts: map[string]string{
config.DefaultScaleDownGpuUtilizationThresholdKey: "0.1",
config.DefaultScaleDownUnneededTimeKey: "1m",
},
expected: &config.NodeGroupAutoscalingOptions{
ScaleDownGpuUtilizationThreshold: 0.1,
ScaleDownUtilizationThreshold: defaultOptions.ScaleDownUtilizationThreshold,
ScaleDownUnneededTime: time.Minute,
ScaleDownUnreadyTime: defaultOptions.ScaleDownUnreadyTime,
},
},
{
desc: "keep defaults on unparsable options values",
opts: map[string]string{
config.DefaultScaleDownGpuUtilizationThresholdKey: "foo",
config.DefaultScaleDownUnneededTimeKey: "bar",
},
expected: defaultOptions,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
mgr := newTestGceManager(t, "", false)
mig := setupTestDefaultPool(mgr, true)
mgr.cache.SetAutoscalingOptions(mig.GceRef(), c.opts)
actual := mgr.GetMigOptions(mig, *defaultOptions)
assert.Equal(t, c.expected, actual)
})
}
}