Skip to content

Commit

Permalink
Added OnDemand and Spot Price models addressing #131
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcrgl committed Nov 22, 2017
1 parent 362b031 commit 1e548ee
Show file tree
Hide file tree
Showing 21 changed files with 447,497 additions and 19 deletions.
46 changes: 46 additions & 0 deletions cluster-autoscaler/cloudprovider/aws/api/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
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 api

import "strconv"

func stringRefToFloat64(p *string) (float64, error) {
if p == nil {
return 0, nil
}
return strconv.ParseFloat(*p, 64)
}

func stringRefToStringSlice(in ...*string) []string {
vs := make([]string, len(in))

for i, v := range in {
vs[i] = *v
}

return vs
}

func stringToStringSliceRef(in ...string) []*string {
vs := make([]*string, len(in))

for i, v := range in {
vs[i] = &v
}

return vs
}
75 changes: 75 additions & 0 deletions cluster-autoscaler/cloudprovider/aws/api/ec2_autoscaling.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
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 api

import (
"fmt"

"github.com/aws/aws-sdk-go/service/autoscaling"
)

type awsEC2AutoscalingGroupService interface {
DescribeAutoScalingGroups(input *autoscaling.DescribeAutoScalingGroupsInput) (*autoscaling.DescribeAutoScalingGroupsOutput, error)
}

// EC2AutoscalingGroup holds AWS Autoscaling Group information
type EC2AutoscalingGroup struct {
Name string
LaunchConfigurationName string
AvailabilityZones []string
}

// NewEC2AutoscalingService is the constructor of autoscalingService which is a wrapper for the AWS EC2
// Autoscaling Group API
func NewEC2AutoscalingService(awsEC2Service awsEC2AutoscalingGroupService) *autoscalingService {
return &autoscalingService{service: awsEC2Service}
}

type autoscalingService struct {
service awsEC2AutoscalingGroupService
}

// DescribeAutoscalingGroup returns the corresponding EC2AutoscalingGroup by the given autoscaling group name
func (ass *autoscalingService) DescribeAutoscalingGroup(autoscalingGroupName string) (*EC2AutoscalingGroup, error) {
req := &autoscaling.DescribeAutoScalingGroupsInput{
AutoScalingGroupNames: []*string{&autoscalingGroupName},
}

for {
res, err := ass.service.DescribeAutoScalingGroups(req)
if err != nil {
return nil, err
}

for _, group := range res.AutoScalingGroups {
if *group.AutoScalingGroupName == autoscalingGroupName {
return &EC2AutoscalingGroup{
Name: *group.AutoScalingGroupName,
LaunchConfigurationName: *group.LaunchConfigurationName,
AvailabilityZones: stringRefToStringSlice(group.AvailabilityZones...),
}, nil
}
}

req.NextToken = res.NextToken
if req.NextToken == nil {
break
}
}

return nil, fmt.Errorf("autoscaling group named %s not found", autoscalingGroupName)
}
152 changes: 152 additions & 0 deletions cluster-autoscaler/cloudprovider/aws/api/ec2_autoscaling_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
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 api

import (
"errors"
"testing"

"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/stretchr/testify/assert"
)

func TestAutoscalingService_DescribeAutoscalingGroup(t *testing.T) {
type testCase struct {
asName string
expectError bool
expectResult bool
}
type cases []testCase

var (
asName1 = "k8s-AutoscalingGroupWorker-TTTTTTTTTTTTT"
asName2 = "k8s-AutoscalingGroupWorker-YYYYYYYYYYYYY"
asName3 = "k8s-AutoscalingGroupWorker-XXXXXXXXXXXXX"
lcName1 = "k8s-LaunchConfigurationWorker-TTTTTTTTTTTTT"
lcName2 = "k8s-LaunchConfigurationWorker-YYYYYYYYYYYYY"
azName1 = "us-east-1a"
azName2 = "us-east-1b"
)

service := NewEC2AutoscalingService(newFakeAutoscalingService(
newAutoscalingMock(asName1, lcName1, azName1, azName2),
newAutoscalingMock(asName2, lcName2, azName1, azName2),
))

tcs := cases{
{ // good case: common case
asName1,
false,
true,
},
{ // good case: common case
asName2,
false,
true,
},
{ // error case: unknown autoscaling group
asName3,
true,
false,
},
}

for id, tc := range tcs {
out, err := service.DescribeAutoscalingGroup(tc.asName)
if tc.expectError {
assert.Error(t, err, fmt.Sprintf("case %d", id))
assert.Nil(t, out, fmt.Sprintf("case %d", id))
} else {
assert.NoError(t, err, fmt.Sprintf("case %d", id))
assert.NotNil(t, out, fmt.Sprintf("case %d", id))
}
if tc.expectResult {
assert.Equal(t, tc.asName, out.Name, fmt.Sprintf("case %d", id))
}

}
}

func newFakeAutoscalingService(ams ...autoscalingMock) *fakeAutoscalingService {
m := make(map[string]*autoscaling.Group)

for _, am := range ams {
m[am.name] = am.asg
}

return &fakeAutoscalingService{m, []string{"token-a", "token-b"}}
}

func newAutoscalingMock(asName, lcName string, availabilityZones ...string) autoscalingMock {
return autoscalingMock{
asg: &autoscaling.Group{
AvailabilityZones: stringToStringSliceRef(availabilityZones...),
LaunchConfigurationName: aws.String(lcName),
AutoScalingGroupName: aws.String(asName),
},
name: asName,
}
}

type autoscalingMock struct {
asg *autoscaling.Group
name string
}

type fakeAutoscalingService struct {
mocks map[string]*autoscaling.Group
tokens []string
}

func (lcs *fakeAutoscalingService) DescribeAutoScalingGroups(input *autoscaling.DescribeAutoScalingGroupsInput) (output *autoscaling.DescribeAutoScalingGroupsOutput, err error) {
output = new(autoscaling.DescribeAutoScalingGroupsOutput)

if len(lcs.tokens) != 0 {
if input.NextToken == nil {
output.NextToken = &lcs.tokens[0]
return
}

for i, token := range lcs.tokens {
if *input.NextToken == token {
next := i + 1
if next < len(lcs.tokens) {
nextToken := lcs.tokens[next]
output.NextToken = &nextToken
} else {
goto respond
}
return
}
}

return nil, errors.New("invalid token")
}

respond:
output.AutoScalingGroups = make([]*autoscaling.Group, 0)
for _, name := range input.AutoScalingGroupNames {
if item, found := lcs.mocks[*name]; found {
output.AutoScalingGroups = append(output.AutoScalingGroups, item)
}
}

return
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
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 api

import (
"fmt"

"github.com/aws/aws-sdk-go/service/autoscaling"
)

type awsEC2LaunchConfigurationService interface {
DescribeLaunchConfigurations(input *autoscaling.DescribeLaunchConfigurationsInput) (*autoscaling.DescribeLaunchConfigurationsOutput, error)
}

// EC2LaunchConfiguration holds AWS EC2 Launch Configuration information
type EC2LaunchConfiguration struct {
HasSpotMarkedBid bool
SpotPrice float64
Name string
InstanceType string
}

// NewEC2LaunchConfigurationService is the constructor of launchConfigurationService which is a wrapper for
// the AWS EC2 LaunchConfiguration API
func NewEC2LaunchConfigurationService(awsEC2Service awsEC2LaunchConfigurationService) *launchConfigurationService {
return &launchConfigurationService{service: awsEC2Service}
}

type launchConfigurationService struct {
service awsEC2LaunchConfigurationService
}

// DescribeLaunchConfiguration returns the corresponding launch configuration by the given launch configuration name.
func (lcs *launchConfigurationService) DescribeLaunchConfiguration(launchConfigurationName string) (*EC2LaunchConfiguration, error) {
req := &autoscaling.DescribeLaunchConfigurationsInput{
LaunchConfigurationNames: []*string{&launchConfigurationName},
}

for {
res, err := lcs.service.DescribeLaunchConfigurations(req)
if err != nil {
return nil, err
}

for _, lc := range res.LaunchConfigurations {
if *lc.LaunchConfigurationName == launchConfigurationName {
p, err := stringRefToFloat64(lc.SpotPrice)
if err != nil {
return nil, fmt.Errorf("failed to parse price: %v", err)
}
return &EC2LaunchConfiguration{
HasSpotMarkedBid: lc.SpotPrice != nil,
SpotPrice: p,
Name: *lc.LaunchConfigurationName,
InstanceType: *lc.InstanceType,
}, nil
}
}

req.NextToken = res.NextToken
if req.NextToken == nil {
break
}
}

return nil, fmt.Errorf("launch configuration named %s not found", launchConfigurationName)
}
Loading

0 comments on commit 1e548ee

Please sign in to comment.