Support autodetection of GCE managed instance groups by name prefix

This commit adds a new usage of the --node-group-auto-discovery flag intended
for use with the GCE cloud provider. GCE instance groups can be automatically
discovered based on a prefix of their group name. Example usage:

--node-group-auto-discovery=mig:prefix=k8s-mig,minNodes=0,maxNodes=10

Note that unlike the existing AWS ASG autodetection functionality we must
specify the min and max nodes in the flag. This is because MIGs store only
a target size in the GCE API - they do not have a min and max size we can
infer via the API.

In order to alleviate this limitation a little we allow multiple uses of the
autodiscovery flag. For example to discover two classes (big and small) of
instance groups with different size limits:

./cluster-autoscaler \
  --node-group-auto-discovery=mig:prefix=k8s-a-small,minNodes=1,maxNodes=10 \
  --node-group-auto-discovery=mig:prefix=k8s-a-big,minNodes=1,maxNodes=100

Zonal clusters (i.e. multizone = false in the cloud config) will detect all
managed instance groups within the cluster's zone. Regional clusters will
detect all matching (zonal) managed instance groups within any of that region's
zones.
This commit is contained in:
Nic Cope 2017-11-10 18:44:31 -08:00
parent 6b2d771ccd
commit 982f9e41a3
12 changed files with 450 additions and 52 deletions

View File

@ -45,12 +45,12 @@ func BuildAwsCloudProvider(awsManager *AwsManager, discoveryOpts cloudprovider.N
return buildStaticallyDiscoveringProvider(awsManager, discoveryOpts.NodeGroupSpecs, resourceLimiter)
}
if discoveryOpts.AutoDiscoverySpecified() {
return buildAutoDiscoveringProvider(awsManager, discoveryOpts.NodeGroupAutoDiscoverySpec, resourceLimiter)
return buildAutoDiscoveringProvider(awsManager, discoveryOpts.NodeGroupAutoDiscoverySpecs, resourceLimiter)
}
return nil, fmt.Errorf("Failed to build an aws cloud provider: Either node group specs or node group auto discovery spec must be specified")
}
func buildAutoDiscoveringProvider(awsManager *AwsManager, spec string, resourceLimiter *cloudprovider.ResourceLimiter) (*awsCloudProvider, error) {
func parseAutoDiscoverySpec(spec string) ([]string, error) {
tokens := strings.Split(spec, ":")
if len(tokens) != 2 {
return nil, fmt.Errorf("Invalid node group auto discovery spec specified via --node-group-auto-discovery: %s", spec)
@ -72,20 +72,36 @@ func buildAutoDiscoveringProvider(awsManager *AwsManager, spec string, resourceL
// Use the k8s cluster name tag to only discover asgs of the cluster denoted by clusterName
// See https://github.com/kubernetes/kubernetes/blob/9ef85a7/pkg/cloudprovider/providers/aws/tags.go#L30-L34
// for more information about the tag
tags := strings.Split(tag, ",")
asgs, err := awsManager.getAutoscalingGroupsByTags(tags)
if err != nil {
return nil, fmt.Errorf("Failed to get ASGs: %v", err)
}
return strings.Split(tag, ","), nil
}
func buildAutoDiscoveringProvider(awsManager *AwsManager, specs []string, resourceLimiter *cloudprovider.ResourceLimiter) (*awsCloudProvider, error) {
aws := &awsCloudProvider{
awsManager: awsManager,
asgs: make([]*Asg, 0),
resourceLimiter: resourceLimiter,
}
for _, asg := range asgs {
aws.addAsg(buildAsg(aws.awsManager, int(*asg.MinSize), int(*asg.MaxSize), *asg.AutoScalingGroupName))
seen := make(map[string]bool)
for _, spec := range specs {
tags, err := parseAutoDiscoverySpec(spec)
if err != nil {
return nil, err
}
asgs, err := awsManager.getAutoscalingGroupsByTags(tags)
if err != nil {
return nil, fmt.Errorf("Failed to get ASGs: %v", err)
}
for _, asg := range asgs {
// An ASG might match more than one provided spec, but we only ever
// want to add it once.
if seen[*asg.AutoScalingGroupARN] {
continue
}
seen[*asg.AutoScalingGroupARN] = true
aws.addAsg(buildAsg(aws.awsManager, int(*asg.MinSize), int(*asg.MaxSize), *asg.AutoScalingGroupName))
}
}
return aws, nil
}

View File

@ -131,6 +131,25 @@ func TestBuildAwsCloudProvider(t *testing.T) {
assert.NoError(t, err)
}
func TestParseAutoDiscoverySpec(t *testing.T) {
want := []string{"coolTag", "anotherTag"}
got, err := parseAutoDiscoverySpec("asg:tag=coolTag,anotherTag")
assert.NoError(t, err)
assert.Equal(t, want, got)
badSpecs := []string{
"asg",
"tag=coolTag,anotherTag",
"mig:tag=coolTag,anotherTag",
"asg:notatag=coolTag,anotherTag",
}
for _, spec := range badSpecs {
_, err = parseAutoDiscoverySpec(spec)
assert.Error(t, err)
}
}
func TestAddNodeGroup(t *testing.T) {
provider := testProvider(t, testAwsManager)
err := provider.addNodeGroup("bad spec")

View File

@ -85,7 +85,7 @@ func (b CloudProviderBuilder) Build(discoveryOpts cloudprovider.NodeGroupDiscove
if gceError != nil {
glog.Fatalf("Failed to create GCE Manager: %v", gceError)
}
cloudProvider, err = gce.BuildGceCloudProvider(gceManager, nodeGroupsFlag, resourceLimiter)
cloudProvider, err = gce.BuildGceCloudProvider(gceManager, discoveryOpts, resourceLimiter)
if err != nil {
glog.Fatalf("Failed to create GCE cloud provider: %v", err)
}

View File

@ -18,6 +18,8 @@ package gce
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
@ -33,8 +35,19 @@ import (
const (
maxAutoprovisionedSize = 1000
minAutoprovisionedSize = 0
autoDiscovererTypeMIG = "mig"
autoDiscovererKeyPrefix = "prefix"
autoDiscovererKeyMinNodes = "min"
autoDiscovererKeyMaxNodes = "max"
)
var validAutoDiscovererKeys = strings.Join([]string{
autoDiscovererKeyPrefix,
autoDiscovererKeyMinNodes,
autoDiscovererKeyMaxNodes,
}, ", ")
// Big machines are temporarily commented out.
// TODO(mwielgus): get this list programatically
var autoprovisionedMachineTypes = []string{
@ -67,11 +80,20 @@ type GceCloudProvider struct {
}
// BuildGceCloudProvider builds CloudProvider implementation for GCE.
func BuildGceCloudProvider(gceManager GceManager, specs []string, resourceLimiter *cloudprovider.ResourceLimiter) (*GceCloudProvider, error) {
if gceManager.getMode() == ModeGKE && len(specs) != 0 {
func BuildGceCloudProvider(gceManager GceManager, do cloudprovider.NodeGroupDiscoveryOptions, resourceLimiter *cloudprovider.ResourceLimiter) (*GceCloudProvider, error) {
if err := do.Validate(); err != nil {
return nil, fmt.Errorf("Failed to build a GCE cloud provider: %v", err)
}
if gceManager.getMode() == ModeGKE && !do.NoDiscoverySpecified() {
return nil, fmt.Errorf("GKE gets nodegroup specification via API, command line specs are not allowed")
}
if do.AutoDiscoverySpecified() {
return buildAutoDiscoveringProvider(gceManager, do.NodeGroupAutoDiscoverySpecs, resourceLimiter)
}
return buildStaticallyDiscoveringProvider(gceManager, do.NodeGroupSpecs, resourceLimiter)
}
func buildStaticallyDiscoveringProvider(gceManager GceManager, specs []string, resourceLimiter *cloudprovider.ResourceLimiter) (*GceCloudProvider, error) {
gce := &GceCloudProvider{
gceManager: gceManager,
resourceLimiterFromFlags: resourceLimiter,
@ -84,6 +106,80 @@ func BuildGceCloudProvider(gceManager GceManager, specs []string, resourceLimite
return gce, nil
}
type autoDiscovererConfig struct {
migRe *regexp.Regexp
minNodes string
maxNodes string
}
func parseAutoDiscoverySpec(spec string) (autoDiscovererConfig, error) {
cfg := autoDiscovererConfig{}
tokens := strings.Split(spec, ":")
if len(tokens) != 2 {
return cfg, fmt.Errorf("spec \"%s\" should be discoverer:key=value,key=value", spec)
}
discoverer := tokens[0]
if discoverer != autoDiscovererTypeMIG {
return cfg, fmt.Errorf("unsupported discoverer specified: %s", discoverer)
}
for _, arg := range strings.Split(tokens[1], ",") {
kv := strings.Split(arg, "=")
k, v := kv[0], kv[1]
switch k {
case autoDiscovererKeyPrefix:
var err error
if cfg.migRe, err = regexp.Compile(fmt.Sprintf("^%s.+", v)); err != nil {
return cfg, fmt.Errorf("invalid instance group name prefix \"%s\" - \"^%s.+\" must be a valid RE2 regexp", v, v)
}
case autoDiscovererKeyMinNodes:
if _, err := strconv.Atoi(v); err != nil {
return cfg, fmt.Errorf("invalid minimum nodes: %s", v)
}
cfg.minNodes = v
case autoDiscovererKeyMaxNodes:
if _, err := strconv.Atoi(v); err != nil {
return cfg, fmt.Errorf("invalid maximum nodes: %s", v)
}
cfg.maxNodes = v
default:
return cfg, fmt.Errorf("unsupported key \"%s\" is specified for discoverer \"%s\". Supported keys are \"%s\"", k, discoverer, validAutoDiscovererKeys)
}
}
return cfg, nil
}
func buildAutoDiscoveringProvider(gceManager GceManager, specs []string, resourceLimiter *cloudprovider.ResourceLimiter) (*GceCloudProvider, error) {
gce := &GceCloudProvider{gceManager: gceManager, resourceLimiterFromFlags: resourceLimiter}
seen := make(map[string]bool)
for _, spec := range specs {
cfg, err := parseAutoDiscoverySpec(spec)
if err != nil {
return nil, fmt.Errorf("invalid node group auto discovery spec \"%s\": %v", spec, err)
}
links, err := gceManager.findMigsNamed(cfg.migRe)
if err != nil {
return nil, fmt.Errorf("cannot autodiscover managed instance groups: %s", err)
}
for _, link := range links {
// A MIG might match more than one provided spec, but we only ever
// want to add it once.
if seen[link] {
continue
}
seen[link] = true
spec := fmt.Sprintf("%s:%s:%s", cfg.minNodes, cfg.maxNodes, link)
if err := gce.addNodeGroup(spec); err != nil {
return nil, err
}
}
}
return gce, nil
}
// Cleanup cleans up all resources before the cloud provider is removed
func (gce *GceCloudProvider) Cleanup() error {
gce.gceManager.Cleanup()

View File

@ -17,9 +17,11 @@ limitations under the License.
package gce
import (
"errors"
"fmt"
"net/http"
"reflect"
"regexp"
"strings"
"testing"
@ -122,7 +124,12 @@ func (m *gceManagerMock) GetResourceLimiter() (*cloudprovider.ResourceLimiter, e
return args.Get(0).(*cloudprovider.ResourceLimiter), args.Error(1)
}
func TestBuildGceCloudProvider(t *testing.T) {
func (m *gceManagerMock) findMigsNamed(name *regexp.Regexp) ([]string, error) {
args := m.Called()
return args.Get(0).([]string), args.Error(1)
}
func TestBuildStaticGceCloudProvider(t *testing.T) {
gceManagerMock := &gceManagerMock{}
ng1Name := "https://content.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroups/ng1"
@ -132,37 +139,122 @@ func TestBuildGceCloudProvider(t *testing.T) {
map[string]int64{cloudprovider.ResourceNameCores: 1, cloudprovider.ResourceNameMemory: 10000000},
map[string]int64{cloudprovider.ResourceNameCores: 10, cloudprovider.ResourceNameMemory: 100000000})
// GCE mode.
// GCE mode with explicit node groups.
gceManagerMock.On("getMode").Return(ModeGCE).Once()
gceManagerMock.On("RegisterMig",
mock.MatchedBy(func(mig *Mig) bool {
return mig.Name == "ng1" || mig.Name == "ng2"
})).Return(true).Times(2)
provider, err := BuildGceCloudProvider(gceManagerMock,
[]string{"0:10:" + ng1Name, "0:5:https:" + ng2Name},
resourceLimiter)
assert.NoError(t, err)
assert.NotNil(t, provider)
mock.AssertExpectationsForObjects(t, gceManagerMock)
// GKE mode.
gceManagerMock.On("getMode").Return(ModeGKE).Once()
provider, err = BuildGceCloudProvider(gceManagerMock, []string{}, resourceLimiter)
do := cloudprovider.NodeGroupDiscoveryOptions{
NodeGroupSpecs: []string{"0:10:" + ng1Name, "0:5:https:" + ng2Name},
}
provider, err := BuildGceCloudProvider(gceManagerMock, do, resourceLimiter)
assert.NoError(t, err)
assert.NotNil(t, provider)
mock.AssertExpectationsForObjects(t, gceManagerMock)
// Error on GKE mode with specs.
gceManagerMock.On("getMode").Return(ModeGKE).Once()
provider, err = BuildGceCloudProvider(gceManagerMock,
[]string{"0:10:" + ng1Name, "0:5:https:" + ng2Name},
resourceLimiter)
_, err = BuildGceCloudProvider(gceManagerMock, do, resourceLimiter)
assert.Error(t, err)
assert.Equal(t, "GKE gets nodegroup specification via API, command line specs are not allowed", err.Error())
mock.AssertExpectationsForObjects(t, gceManagerMock)
// Ensure GKE mode works with no specs.
gceManagerMock.On("getMode").Return(ModeGKE).Once()
do = cloudprovider.NodeGroupDiscoveryOptions{}
provider, err = BuildGceCloudProvider(gceManagerMock, do, resourceLimiter)
assert.NoError(t, err)
assert.NotNil(t, provider)
mock.AssertExpectationsForObjects(t, gceManagerMock)
// Error with both explicit and autodiscovery specs.
do = cloudprovider.NodeGroupDiscoveryOptions{
NodeGroupSpecs: []string{"0:10:" + ng1Name, "0:5:https:" + ng2Name},
NodeGroupAutoDiscoverySpecs: []string{"mig:prefix=pfx,min=0,max=10"},
}
_, err = BuildGceCloudProvider(gceManagerMock, do, resourceLimiter)
assert.Error(t, err)
mock.AssertExpectationsForObjects(t, gceManagerMock)
}
func TestBuildAutodiscoveringGceCloudProvider(t *testing.T) {
gceManagerMock := &gceManagerMock{}
ng1Name := "https://content.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroups/ng1"
ng2Name := "https://content.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroups/ng2"
resourceLimiter := cloudprovider.NewResourceLimiter(
map[string]int64{cloudprovider.ResourceNameCores: 1, cloudprovider.ResourceNameMemory: 10000000},
map[string]int64{cloudprovider.ResourceNameCores: 10, cloudprovider.ResourceNameMemory: 100000000})
// GCE mode with autodiscovery.
gceManagerMock.On("getMode").Return(ModeGCE).Once()
gceManagerMock.On("findMigsNamed").Return([]string{ng1Name, ng2Name}, nil).Twice()
gceManagerMock.On("RegisterMig",
mock.MatchedBy(func(mig *Mig) bool {
return mig.Name == "ng1" || mig.Name == "ng2"
})).Return(true).Times(2)
do := cloudprovider.NodeGroupDiscoveryOptions{
NodeGroupAutoDiscoverySpecs: []string{
"mig:prefix=ng,min=0,max=10",
"mig:prefix=n,min=1,max=2",
},
}
provider, err := BuildGceCloudProvider(gceManagerMock, do, resourceLimiter)
assert.NoError(t, err)
assert.NotNil(t, provider)
mock.AssertExpectationsForObjects(t, gceManagerMock)
// Error finding instance groups
gceManagerMock.On("getMode").Return(ModeGCE).Once()
gceManagerMock.On("findMigsNamed").Return([]string{}, errors.New("nope")).Once()
_, err = BuildGceCloudProvider(gceManagerMock, do, resourceLimiter)
assert.Error(t, err)
assert.Equal(t, "cannot autodiscover managed instance groups: nope", err.Error())
mock.AssertExpectationsForObjects(t, gceManagerMock)
// Error on GKE mode with autodiscovery specs.
gceManagerMock.On("getMode").Return(ModeGKE).Once()
_, err = BuildGceCloudProvider(gceManagerMock, do, resourceLimiter)
assert.Error(t, err)
assert.Equal(t, "GKE gets nodegroup specification via API, command line specs are not allowed", err.Error())
mock.AssertExpectationsForObjects(t, gceManagerMock)
// Bad autodiscovery spec
do = cloudprovider.NodeGroupDiscoveryOptions{
NodeGroupAutoDiscoverySpecs: []string{"mig"},
}
gceManagerMock.On("getMode").Return(ModeGCE).Once()
_, err = BuildGceCloudProvider(gceManagerMock, do, resourceLimiter)
assert.Error(t, err)
mock.AssertExpectationsForObjects(t, gceManagerMock)
}
func TestParseAutoDiscoverySpec(t *testing.T) {
want := autoDiscovererConfig{
migRe: regexp.MustCompile("^pfx.+"),
minNodes: "0",
maxNodes: "10",
}
got, err := parseAutoDiscoverySpec("mig:prefix=pfx,min=0,max=10")
assert.NoError(t, err)
assert.Equal(t, want, got)
badSpecs := []string{
"prefix=pfx,min=0,max=10",
"asg:prefix=pfx,min=0,max=10",
"mig:prefix=pfx,min=0,max=10,unknown=hi",
"mig:prefix=pfx,min=a,max=10",
"mig:prefix=pfx,min=10,max=donkey",
"mig:prefix=(a,min=1,max=10",
}
for _, spec := range badSpecs {
_, err = parseAutoDiscoverySpec(spec)
assert.Error(t, err)
}
}
func TestNodeGroups(t *testing.T) {

View File

@ -17,11 +17,15 @@ limitations under the License.
package gce
import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"path"
"reflect"
"regexp"
"strings"
"sync"
"time"
@ -125,6 +129,7 @@ type GceManager interface {
getProjectId() string
getMode() GcpCloudProviderMode
getTemplates() *templateBuilder
findMigsNamed(name *regexp.Regexp) ([]string, error)
}
// gceManagerImpl handles gce communication and data caching.
@ -730,7 +735,7 @@ func (m *gceManagerImpl) DeleteInstances(instances []*GceRef) error {
return err
}
if mig != commonMig {
return fmt.Errorf("Connot delete instances which don't belong to the same MIG.")
return errors.New("Cannot delete instances which don't belong to the same MIG.")
}
}
@ -946,3 +951,57 @@ func getProjectAndLocation(isRegional bool) (string, string, error) {
}
return projectID, location, nil
}
func (m *gceManagerImpl) findMigsNamed(name *regexp.Regexp) ([]string, error) {
if m.isRegional {
return m.findMigsInRegion(m.location, name)
}
return m.findMigsInZone(m.location, name)
}
func (m *gceManagerImpl) getZones(region string) ([]string, error) {
r, err := m.gceService.Regions.Get(m.getProjectId(), region).Do()
if err != nil {
return nil, fmt.Errorf("cannot get zones for GCE region %s: %v", region, err)
}
zones := make([]string, len(r.Zones))
for i, link := range r.Zones {
zones[i] = path.Base(link)
}
return zones, nil
}
func (m *gceManagerImpl) findMigsInRegion(region string, name *regexp.Regexp) ([]string, error) {
links := make([]string, 0)
zones, err := m.getZones(region)
if err != nil {
return nil, err
}
for _, z := range zones {
zl, err := m.findMigsInZone(z, name)
if err != nil {
return nil, err
}
for _, link := range zl {
links = append(links, link)
}
}
return links, nil
}
func (m *gceManagerImpl) findMigsInZone(zone string, name *regexp.Regexp) ([]string, error) {
filter := fmt.Sprintf("name eq %s", name)
links := make([]string, 0)
req := m.gceService.InstanceGroups.List(m.getProjectId(), zone).Filter(filter)
if err := req.Pages(context.TODO(), func(page *gce.InstanceGroupList) error {
for _, ig := range page.Items {
links = append(links, ig.SelfLink)
glog.V(3).Infof("autodiscovered managed instance group %s using regexp %s", ig.Name, name)
}
return nil
}); err != nil {
return nil, fmt.Errorf("cannot list managed instance groups: %v", err)
}
return links, nil
}

View File

@ -19,6 +19,7 @@ package gce
import (
"fmt"
"net/http"
"regexp"
"testing"
. "k8s.io/autoscaler/cluster-autoscaler/utils/test"
@ -215,7 +216,7 @@ const instanceGroupManager = `{
"kind": "compute#instanceGroupManager",
"id": "3213213219",
"creationTimestamp": "2017-09-15T04:47:24.687-07:00",
"name": "gke-cluster-1-default-pool",
"name": "%s",
"zone": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s",
"instanceTemplate": "https://www.googleapis.com/compute/v1/projects/project1/global/instanceTemplates/gke-cluster-1-default-pool",
"instanceGroup": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups/gke-cluster-1-default-pool",
@ -232,9 +233,10 @@ const instanceGroupManager = `{
"refreshing": 0
},
"targetSize": 3,
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroupManagers/gke-cluster-1-default-pool"
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroupManagers/%s"
}
`
const instanceTemplate = `
{
"kind": "compute#instanceTemplate",
@ -492,7 +494,11 @@ const getClusterResponse = `{
}`
func getInstanceGroupManager(zone string) string {
return fmt.Sprintf(instanceGroupManager, zone, zone, zone)
return getInstanceGroupManagerNamed("gke-cluster-1-default-pool", zone)
}
func getInstanceGroupManagerNamed(name, zone string) string {
return fmt.Sprintf(instanceGroupManager, name, zone, zone, zone, name)
}
func getMachineType(zone string) string {
@ -936,7 +942,7 @@ func TestDeleteInstances(t *testing.T) {
err = g.DeleteInstances(instances)
assert.Error(t, err)
assert.Equal(t, "Connot delete instances which don't belong to the same MIG.", err.Error())
assert.Equal(t, "Cannot delete instances which don't belong to the same MIG.", err.Error())
mock.AssertExpectationsForObjects(t, server)
}
@ -1116,3 +1122,98 @@ func TestFetchResourceLimiter(t *testing.T) {
mock.AssertExpectationsForObjects(t, server)
}
const instanceGroup = `{
"kind": "compute#instanceGroup",
"id": "1121230570947910218",
"name": "%s",
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups/%s",
"size": 1
}`
func getInstanceGroup(zone string) string {
return getInstanceGroupNamed("gke-cluster-1-default-pool", zone)
}
func getInstanceGroupNamed(name, zone string) string {
return fmt.Sprintf(instanceGroup, name, zone, name)
}
const instanceGroupList = `{
"kind": "compute#instanceGroupList",
"id": "projects/project1a/zones/%s/instanceGroups",
"items": [%s, %s],
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/zones/%s/instanceGroups"
}`
func listInstanceGroups(zone string) string {
return fmt.Sprintf(instanceGroupList,
zone,
getInstanceGroupNamed("gce-pool-a", zone),
getInstanceGroupNamed("gce-pool-b", zone),
zone,
)
}
func TestFindMigsNamedZonal(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
server.On("handle", "/project1/zones/us-central1-b/instanceGroups").Return(listInstanceGroups("us-central1-b")).Once()
regional := false
g := newTestGceManager(t, server.URL, ModeGCE, regional)
links, err := g.findMigsNamed(regexp.MustCompile("^UNUSED"))
assert.NoError(t, err)
assert.Equal(t, 2, len(links))
assert.Equal(t, "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroups/gce-pool-a", links[0])
assert.Equal(t, "https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroups/gce-pool-b", links[1])
mock.AssertExpectationsForObjects(t, server)
}
const getRegion = `{
"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-a",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-c",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-f"
],
"quotas": [],
"selfLink": "https://www.googleapis.com/compute/v1/projects/project1/regions/us-central1"
}`
func TestFindMigsNamedRegional(t *testing.T) {
server := NewHttpServerMock()
defer server.Close()
server.On("handle", "/project1/regions/us-central1").Return(getRegion).Once()
server.On("handle", "/project1/zones/us-central1-a/instanceGroups").Return(listInstanceGroups("us-central1-a")).Once()
server.On("handle", "/project1/zones/us-central1-b/instanceGroups").Return(listInstanceGroups("us-central1-b")).Once()
server.On("handle", "/project1/zones/us-central1-c/instanceGroups").Return(listInstanceGroups("us-central1-c")).Once()
server.On("handle", "/project1/zones/us-central1-f/instanceGroups").Return(listInstanceGroups("us-central1-f")).Once()
regional := true
g := newTestGceManager(t, server.URL, ModeGCE, regional)
got, err := g.findMigsNamed(regexp.MustCompile("^UNUSED"))
assert.NoError(t, err)
want := []string{
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/instanceGroups/gce-pool-a",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-a/instanceGroups/gce-pool-b",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroups/gce-pool-a",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-b/instanceGroups/gce-pool-b",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-c/instanceGroups/gce-pool-a",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-c/instanceGroups/gce-pool-b",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-f/instanceGroups/gce-pool-a",
"https://www.googleapis.com/compute/v1/projects/project1/zones/us-central1-f/instanceGroups/gce-pool-b",
}
assert.Equal(t, want, got)
mock.AssertExpectationsForObjects(t, server)
}

View File

@ -23,23 +23,29 @@ type NodeGroupDiscoveryOptions struct {
// NodeGroupSpecs is specified to statically discover node groups listed in it
NodeGroupSpecs []string
// NodeGroupAutoDiscoverySpec is specified for automatically discovering node groups according to the specs
NodeGroupAutoDiscoverySpec string
NodeGroupAutoDiscoverySpecs []string
}
// StaticDiscoverySpecified returns true only when there are 1 or more --nodes flags are specified
// StaticDiscoverySpecified returns true only when there are 1 or more --nodes flags specified
func (o NodeGroupDiscoveryOptions) StaticDiscoverySpecified() bool {
return len(o.NodeGroupSpecs) > 0
}
// AutoDiscoverySpecified returns true only when there is --node-group-auto-discovery specified
// AutoDiscoverySpecified returns true only when there are 1 or more --node-group-auto-discovery flags specified
func (o NodeGroupDiscoveryOptions) AutoDiscoverySpecified() bool {
return o.NodeGroupAutoDiscoverySpec != ""
return len(o.NodeGroupAutoDiscoverySpecs) > 0
}
// NoDiscoverySpecified returns true expected nly when there were no --nodes or
// --node-group-auto-discovery flags specified. This is expected in GKE.
func (o NodeGroupDiscoveryOptions) NoDiscoverySpecified() bool {
return !o.StaticDiscoverySpecified() && !o.AutoDiscoverySpecified()
}
// Validate returns and error when both --nodes and --node-group-auto-discovery are specified
func (o NodeGroupDiscoveryOptions) Validate() error {
if o.StaticDiscoverySpecified() && o.AutoDiscoverySpecified() {
return fmt.Errorf("Either node group specs(%v) or node group auto discovery spec(%v) can be specified but not both", o.NodeGroupSpecs, o.NodeGroupAutoDiscoverySpec)
return fmt.Errorf("Either node group specs(%v) or node group auto discovery spec(%v) can be specified but not both", o.NodeGroupSpecs, o.NodeGroupAutoDiscoverySpecs)
}
return nil
}

View File

@ -25,8 +25,8 @@ import (
func TestNodeGroupDiscoveryOptionsValidate(t *testing.T) {
o := NodeGroupDiscoveryOptions{
NodeGroupAutoDiscoverySpec: "asg:tag=foobar",
NodeGroupSpecs: []string{"myasg:0:10"},
NodeGroupAutoDiscoverySpecs: []string{"asg:tag=foobar"},
NodeGroupSpecs: []string{"myasg:0:10"},
}
err := o.Validate()
@ -34,12 +34,15 @@ func TestNodeGroupDiscoveryOptionsValidate(t *testing.T) {
t.Errorf("Expected validation error didn't occur with NodeGroupDiscoveryOptions: %+v", o)
t.FailNow()
}
if msg := fmt.Sprintf("%v", err); msg != `Either node group specs([myasg:0:10]) or node group auto discovery spec(asg:tag=foobar) can be specified but not both` {
if msg := fmt.Sprintf("%v", err); msg != `Either node group specs([myasg:0:10]) or node group auto discovery spec([asg:tag=foobar]) can be specified but not both` {
t.Errorf("Unexpected validation error message: %s", msg)
}
o = NodeGroupDiscoveryOptions{
NodeGroupAutoDiscoverySpec: "asg:tag=foobar",
NodeGroupAutoDiscoverySpecs: []string{
"mig:prefix=iga,min=0,max=10",
"mig:prefix=igb,min=0,max=20",
},
}
assert.NoError(t, o.Validate())

View File

@ -54,13 +54,13 @@ func NewAutoscaler(opts AutoscalerOptions, predicateChecker *simulator.Predicate
autoscalerBuilder := NewAutoscalerBuilder(opts.AutoscalingOptions, predicateChecker, kubeClient, kubeEventRecorder, listerRegistry)
if opts.ConfigMapName != "" {
if opts.NodeGroupAutoDiscovery != "" {
if len(opts.NodeGroupAutoDiscovery) > 0 {
glog.Warning("Both --configmap and --node-group-auto-discovery were specified but only the former is going to take effect")
}
configFetcher := dynamic.NewConfigFetcher(opts.ConfigFetcherOptions, kubeClient, kubeEventRecorder)
return NewDynamicAutoscaler(autoscalerBuilder, configFetcher)
}
if opts.NodeGroupAutoDiscovery != "" {
if len(opts.NodeGroupAutoDiscovery) > 0 {
return NewPollingAutoscaler(autoscalerBuilder)
}
return autoscalerBuilder.Build()

View File

@ -76,7 +76,7 @@ type AutoscalingOptions struct {
// MinMemoryTotal sets the maximum memory (in megabytes) in the whole cluster
MinMemoryTotal int64
// NodeGroupAutoDiscovery represents one or more definition(s) of node group auto-discovery
NodeGroupAutoDiscovery string
NodeGroupAutoDiscovery []string
// EstimatorName is the estimator used to estimate the number of needed nodes in scale up.
EstimatorName string
// ExpanderName sets the type of node group expander to be used in scale up
@ -141,8 +141,8 @@ func NewAutoscalingContext(options AutoscalingOptions, predicateChecker *simulat
cloudProviderBuilder := builder.NewCloudProviderBuilder(options.CloudProviderName, options.CloudConfig, options.ClusterName, options.NodeAutoprovisioningEnabled)
cloudProvider := cloudProviderBuilder.Build(cloudprovider.NodeGroupDiscoveryOptions{
NodeGroupSpecs: options.NodeGroups,
NodeGroupAutoDiscoverySpec: options.NodeGroupAutoDiscovery},
NodeGroupSpecs: options.NodeGroups,
NodeGroupAutoDiscoverySpecs: options.NodeGroupAutoDiscovery},
cloudprovider.NewResourceLimiter(
map[string]int64{cloudprovider.ResourceNameCores: int64(options.MinCoresTotal), cloudprovider.ResourceNameMemory: options.MinMemoryTotal},
map[string]int64{cloudprovider.ResourceNameCores: options.MaxCoresTotal, cloudprovider.ResourceNameMemory: options.MaxMemoryTotal}))

View File

@ -65,7 +65,9 @@ func (flag *MultiStringFlag) Set(value string) error {
}
var (
nodeGroupsFlag MultiStringFlag
nodeGroupsFlag MultiStringFlag
nodeGroupAutoDiscoveryFlag MultiStringFlag
clusterName = flag.String("cluster-name", "", "Autoscaled cluster name, if available")
address = flag.String("address", ":8085", "The address to expose prometheus metrics.")
kubernetes = flag.String("kubernetes", "", "Kubernetes master location. Leave blank for default")
@ -73,7 +75,6 @@ var (
cloudConfig = flag.String("cloud-config", "", "The path to the cloud provider configuration file. Empty string for no configuration file.")
configMapName = flag.String("configmap", "", "The name of the ConfigMap containing settings used for dynamic reconfiguration. Empty string for no ConfigMap.")
namespace = flag.String("namespace", "kube-system", "Namespace in which cluster-autoscaler run. If a --configmap flag is also provided, ensure that the configmap exists in this namespace before CA runs.")
nodeGroupAutoDiscovery = flag.String("node-group-auto-discovery", "", "One or more definition(s) of node group auto-discovery. A definition is expressed `<name of discoverer per cloud provider>:[<key>[=<value>]]`. Only the `aws` cloud provider is currently supported. The only valid discoverer for it is `asg` and the valid key is `tag`. For example, specifying `--cloud-provider aws` and `--node-group-auto-discovery asg:tag=cluster-autoscaler/auto-discovery/enabled,kubernetes.io/cluster/<YOUR CLUSTER NAME>` results in ASGs tagged with `cluster-autoscaler/auto-discovery/enabled` and `kubernetes.io/cluster/<YOUR CLUSTER NAME>` to be considered as target node groups")
scaleDownEnabled = flag.Bool("scale-down-enabled", true, "Should CA scale down the cluster")
scaleDownDelayAfterAdd = flag.Duration("scale-down-delay-after-add", 10*time.Minute,
"How long after scale up that scale down evaluation resumes")
@ -146,7 +147,7 @@ func createAutoscalerOptions() core.AutoscalerOptions {
autoscalingOpts := core.AutoscalingOptions{
CloudConfig: *cloudConfig,
CloudProviderName: *cloudProviderFlag,
NodeGroupAutoDiscovery: *nodeGroupAutoDiscovery,
NodeGroupAutoDiscovery: nodeGroupAutoDiscoveryFlag,
MaxTotalUnreadyPercentage: *maxTotalUnreadyPercentage,
OkTotalUnreadyCount: *okTotalUnreadyCount,
EstimatorName: *estimatorFlag,
@ -280,6 +281,11 @@ func main() {
bindFlags(&leaderElection, pflag.CommandLine)
flag.Var(&nodeGroupsFlag, "nodes", "sets min,max size and other configuration data for a node group in a format accepted by cloud provider."+
"Can be used multiple times. Format: <min>:<max>:<other...>")
flag.Var(&nodeGroupAutoDiscoveryFlag, "node-group-auto-discovery", "One or more definition(s) of node group auto-discovery. "+
"A definition is expressed `<name of discoverer>:[<key>[=<value>]]`. "+
"The `aws` and `gce` cloud providers are currently supported. AWS matches by ASG tags, e.g. `asg:tag=tagKey,anotherTagKey`. "+
"GCE matches by IG prefix, and requires you to specify min and max nodes per IG, e.g. `mig:prefix=pfx,min=0,max=10` "+
"Can be used multiple times.")
kube_flag.InitFlags()
healthCheck := metrics.NewHealthCheck(*maxInactivityTimeFlag, *maxFailingTimeFlag)