diff --git a/cmd/apiserver/plugins.go b/cmd/apiserver/plugins.go index 266b57d108a..3eddf5c0545 100644 --- a/cmd/apiserver/plugins.go +++ b/cmd/apiserver/plugins.go @@ -20,6 +20,7 @@ package main // This should probably be part of some configuration fed into the build for a // given binary target. import ( + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt" diff --git a/pkg/cloudprovider/aws/aws.go b/pkg/cloudprovider/aws/aws.go new file mode 100644 index 00000000000..438b263077f --- /dev/null +++ b/pkg/cloudprovider/aws/aws.go @@ -0,0 +1,181 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 aws_cloud + +import ( + "fmt" + "io" + "net" + "regexp" + + "code.google.com/p/gcfg" + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" +) + +type EC2 interface { + Instances(instIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) +} + +// AWSCloud is an implementation of Interface, TCPLoadBalancer and Instances for Amazon Web Services. +type AWSCloud struct { + ec2 EC2 + cfg *AWSCloudConfig +} + +type AWSCloudConfig struct { + Global struct { + Region string + } +} + +type AuthFunc func() (auth aws.Auth, err error) + +func init() { + cloudprovider.RegisterCloudProvider("aws", func(config io.Reader) (cloudprovider.Interface, error) { + return newAWSCloud(config, getAuth) + }) +} + +func getAuth() (auth aws.Auth, err error) { + return aws.GetAuth("", "") +} + +// readAWSCloudConfig reads an instance of AWSCloudConfig from config reader. +func readAWSCloudConfig(config io.Reader) (*AWSCloudConfig, error) { + if config == nil { + return nil, fmt.Errorf("No AWS cloud provider config file given") + } + + var cfg AWSCloudConfig + err := gcfg.ReadInto(&cfg, config) + if err != nil { + return nil, err + } + + if cfg.Global.Region == "" { + return nil, fmt.Errorf("No region specified in configuration file") + } + + return &cfg, nil +} + +// newAWSCloud creates a new instance of AWSCloud. +func newAWSCloud(config io.Reader, authFunc AuthFunc) (*AWSCloud, error) { + cfg, err := readAWSCloudConfig(config) + if err != nil { + return nil, fmt.Errorf("Unable to read AWS cloud provider config file: %s", err) + } + + auth, err := authFunc() + if err != nil { + return nil, err + } + + region, ok := aws.Regions[cfg.Global.Region] + if !ok { + return nil, fmt.Errorf("Not a valid AWS region: %s", cfg.Global.Region) + } + + ec2 := ec2.New(auth, region) + return &AWSCloud{ + ec2: ec2, + cfg: cfg, + }, nil +} + +// TCPLoadBalancer returns an implementation of TCPLoadBalancer for Amazon Web Services. +func (aws *AWSCloud) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { + return nil, false +} + +// Instances returns an implementation of Instances for Amazon Web Services. +func (aws *AWSCloud) Instances() (cloudprovider.Instances, bool) { + return aws, true +} + +// Zones returns an implementation of Zones for Amazon Web Services. +func (aws *AWSCloud) Zones() (cloudprovider.Zones, bool) { + return nil, false +} + +// IPAddress is an implementation of Instances.IPAddress. +func (aws *AWSCloud) IPAddress(name string) (net.IP, error) { + f := ec2.NewFilter() + f.Add("private-dns-name", name) + + resp, err := aws.ec2.Instances(nil, f) + if err != nil { + return nil, err + } + if len(resp.Reservations) == 0 { + return nil, fmt.Errorf("No reservations found for host: %s", name) + } + if len(resp.Reservations) > 1 { + return nil, fmt.Errorf("Multiple reservations found for host: %s", name) + } + if len(resp.Reservations[0].Instances) == 0 { + return nil, fmt.Errorf("No instances found for host: %s", name) + } + if len(resp.Reservations[0].Instances) > 1 { + return nil, fmt.Errorf("Multiple instances found for host: %s", name) + } + + ipAddress := resp.Reservations[0].Instances[0].PrivateIpAddress + ip := net.ParseIP(ipAddress) + if ip == nil { + return nil, fmt.Errorf("Invalid network IP: %s", ipAddress) + } + return ip, nil +} + +// Return a list of instances matching regex string. +func (aws *AWSCloud) getInstancesByRegex(regex string) ([]string, error) { + resp, err := aws.ec2.Instances(nil, nil) + if err != nil { + return []string{}, err + } + if resp == nil { + return []string{}, fmt.Errorf("No InstanceResp returned") + } + + re, err := regexp.Compile(regex) + if err != nil { + return []string{}, err + } + + instances := []string{} + for _, reservation := range resp.Reservations { + for _, instance := range reservation.Instances { + for _, tag := range instance.Tags { + if tag.Key == "Name" && re.MatchString(tag.Value) { + instances = append(instances, instance.PrivateDNSName) + break + } + } + } + } + return instances, nil +} + +// List is an implementation of Instances.List. +func (aws *AWSCloud) List(filter string) ([]string, error) { + // TODO: Should really use tag query. No need to go regexp. + return aws.getInstancesByRegex(filter) +} diff --git a/pkg/cloudprovider/aws/aws_test.go b/pkg/cloudprovider/aws/aws_test.go new file mode 100644 index 00000000000..bc026db3d01 --- /dev/null +++ b/pkg/cloudprovider/aws/aws_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 aws_cloud + +import ( + "reflect" + "strings" + "testing" + + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" +) + +func TestReadAWSCloudConfig(t *testing.T) { + _, err1 := readAWSCloudConfig(nil) + if err1 == nil { + t.Errorf("Should error when no config reader is given") + } + + _, err2 := readAWSCloudConfig(strings.NewReader("")) + if err2 == nil { + t.Errorf("Should error when config is empty") + } + + _, err3 := readAWSCloudConfig(strings.NewReader("[global]\n")) + if err3 == nil { + t.Errorf("Should error when no region is specified") + } + + cfg, err4 := readAWSCloudConfig(strings.NewReader("[global]\nregion = eu-west-1")) + if err4 != nil { + t.Errorf("Should succeed when a region is specified: %s", err4) + } + if cfg.Global.Region != "eu-west-1" { + t.Errorf("Should read region from config") + } +} + +func TestNewAWSCloud(t *testing.T) { + fakeAuthFunc := func() (auth aws.Auth, err error) { + return aws.Auth{"", "", ""}, nil + } + + _, err1 := newAWSCloud(nil, fakeAuthFunc) + if err1 == nil { + t.Errorf("Should error when no config reader is given") + } + + _, err2 := newAWSCloud(strings.NewReader( + "[global]\nregion = blahonga"), + fakeAuthFunc) + if err2 == nil { + t.Errorf("Should error when config specifies invalid region") + } + + _, err3 := newAWSCloud( + strings.NewReader("[global]\nregion = eu-west-1"), + fakeAuthFunc) + if err3 != nil { + t.Errorf("Should succeed when a valid region is specified: %s", err3) + } +} + +type FakeEC2 struct { + instances func(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) +} + +func (ec2 *FakeEC2) Instances(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) { + return ec2.instances(instanceIds, filter) +} + +func mockInstancesResp(instances []ec2.Instance) (aws *AWSCloud) { + return &AWSCloud{ + &FakeEC2{ + func(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) { + return &ec2.InstancesResp{"", + []ec2.Reservation{ + ec2.Reservation{"", "", "", nil, instances}}}, nil + }}, + nil} +} + +func TestList(t *testing.T) { + instances := make([]ec2.Instance, 4) + instances[0].Tags = []ec2.Tag{ec2.Tag{"Name", "foo"}} + instances[0].PrivateDNSName = "instance1" + instances[1].Tags = []ec2.Tag{ec2.Tag{"Name", "bar"}} + instances[1].PrivateDNSName = "instance2" + instances[2].Tags = []ec2.Tag{ec2.Tag{"Name", "baz"}} + instances[2].PrivateDNSName = "instance3" + instances[3].Tags = []ec2.Tag{ec2.Tag{"Name", "quux"}} + instances[3].PrivateDNSName = "instance4" + + aws := mockInstancesResp(instances) + + table := []struct { + input string + expect []string + }{ + {"blahonga", []string{}}, + {"quux", []string{"instance4"}}, + {"a", []string{"instance2", "instance3"}}, + } + + for _, item := range table { + result, err := aws.List(item.input) + if err != nil { + t.Errorf("Expected call with %v to succeed, failed with %s", item.input, err) + } + if e, a := item.expect, result; !reflect.DeepEqual(e, a) { + t.Errorf("Expected %v, got %v", e, a) + } + } +} + +func TestIPAddress(t *testing.T) { + instances := make([]ec2.Instance, 2) + instances[0].PrivateDNSName = "instance1" + instances[0].PrivateIpAddress = "192.168.0.1" + instances[1].PrivateDNSName = "instance2" + instances[1].PrivateIpAddress = "192.168.0.2" + + aws1 := mockInstancesResp([]ec2.Instance{}) + _, err1 := aws1.IPAddress("instance") + if err1 == nil { + t.Errorf("Should error when no instance found") + } + + aws2 := mockInstancesResp(instances) + _, err2 := aws2.IPAddress("instance1") + if err2 == nil { + t.Errorf("Should error when multiple instances found") + } + + aws3 := mockInstancesResp(instances[0:1]) + ip3, err3 := aws3.IPAddress("instance1") + if err3 != nil { + t.Errorf("Should not error when instance found") + } + if e, a := instances[0].PrivateIpAddress, ip3.String(); e != a { + t.Errorf("Expected %v, got %v", e, a) + } +}