diff --git a/coreos-cloudinit.go b/coreos-cloudinit.go index b64d5208..be613c39 100644 --- a/coreos-cloudinit.go +++ b/coreos-cloudinit.go @@ -34,6 +34,7 @@ import ( "github.com/coreos/coreos-cloudinit/datasource/metadata/cloudsigma" "github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean" "github.com/coreos/coreos-cloudinit/datasource/metadata/ec2" + "github.com/coreos/coreos-cloudinit/datasource/metadata/gce" "github.com/coreos/coreos-cloudinit/datasource/metadata/packet" "github.com/coreos/coreos-cloudinit/datasource/proc_cmdline" "github.com/coreos/coreos-cloudinit/datasource/url" @@ -61,6 +62,7 @@ var ( waagent string metadataService bool ec2MetadataService string + gceMetadataService string cloudSigmaMetadataService bool digitalOceanMetadataService string packetMetadataService string @@ -86,6 +88,7 @@ func init() { flag.StringVar(&flags.sources.waagent, "from-waagent", "", "Read data from provided waagent directory") flag.BoolVar(&flags.sources.metadataService, "from-metadata-service", false, "[DEPRECATED - Use -from-ec2-metadata] Download data from metadata service") flag.StringVar(&flags.sources.ec2MetadataService, "from-ec2-metadata", "", "Download EC2 data from the provided url") + flag.StringVar(&flags.sources.gceMetadataService, "from-gce-metadata", "", "Download GCE data from the provided url") flag.BoolVar(&flags.sources.cloudSigmaMetadataService, "from-cloudsigma-metadata", false, "Download data from CloudSigma server context") flag.StringVar(&flags.sources.digitalOceanMetadataService, "from-digitalocean-metadata", "", "Download DigitalOcean data from the provided url") flag.StringVar(&flags.sources.packetMetadataService, "from-packet-metadata", "", "Download Packet data from metadata service") @@ -112,6 +115,9 @@ var ( "from-ec2-metadata": "http://169.254.169.254/", "from-configdrive": "/media/configdrive", }, + "gce": { + "from-gce-metadata": "http://metadata.google.internal/", + }, "rackspace-onmetal": { "from-configdrive": "/media/configdrive", "convert-netconf": "debian", @@ -174,7 +180,7 @@ func main() { dss := getDatasources() if len(dss) == 0 { - fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-cloudsigma-metadata, --from-packet-metadata, --from-digitalocean-metadata, --from-vmware-guestinfo, --from-waagent, --from-url or --from-proc-cmdline") + fmt.Println("Provide at least one of --from-file, --from-configdrive, --from-ec2-metadata, --from-gce-metadata, --from-cloudsigma-metadata, --from-packet-metadata, --from-digitalocean-metadata, --from-vmware-guestinfo, --from-waagent, --from-url or --from-proc-cmdline") os.Exit(2) } @@ -322,6 +328,9 @@ func getDatasources() []datasource.Datasource { if flags.sources.ec2MetadataService != "" { dss = append(dss, ec2.NewDatasource(flags.sources.ec2MetadataService)) } + if flags.sources.gceMetadataService != "" { + dss = append(dss, gce.NewDatasource(flags.sources.gceMetadataService)) + } if flags.sources.cloudSigmaMetadataService { dss = append(dss, cloudsigma.NewServerContextService()) } diff --git a/datasource/metadata/digitalocean/metadata.go b/datasource/metadata/digitalocean/metadata.go index fa6c605c..71982d0c 100644 --- a/datasource/metadata/digitalocean/metadata.go +++ b/datasource/metadata/digitalocean/metadata.go @@ -66,7 +66,7 @@ type metadataService struct { } func NewDatasource(root string) *metadataService { - return &metadataService{MetadataService: metadata.NewDatasource(root, apiVersion, userdataUrl, metadataPath)} + return &metadataService{MetadataService: metadata.NewDatasource(root, apiVersion, userdataUrl, metadataPath, nil)} } func (ms *metadataService) FetchMetadata() (metadata datasource.Metadata, err error) { diff --git a/datasource/metadata/ec2/metadata.go b/datasource/metadata/ec2/metadata.go index 141fdc94..5489e1d3 100644 --- a/datasource/metadata/ec2/metadata.go +++ b/datasource/metadata/ec2/metadata.go @@ -39,7 +39,7 @@ type metadataService struct { } func NewDatasource(root string) *metadataService { - return &metadataService{metadata.NewDatasource(root, apiVersion, userdataPath, metadataPath)} + return &metadataService{metadata.NewDatasource(root, apiVersion, userdataPath, metadataPath, nil)} } func (ms metadataService) FetchMetadata() (datasource.Metadata, error) { diff --git a/datasource/metadata/gce/metadata.go b/datasource/metadata/gce/metadata.go new file mode 100644 index 00000000..494741c7 --- /dev/null +++ b/datasource/metadata/gce/metadata.go @@ -0,0 +1,89 @@ +// Copyright 2016 CoreOS, Inc. +// +// 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" + "net" + "net/http" + + "github.com/coreos/coreos-cloudinit/datasource" + "github.com/coreos/coreos-cloudinit/datasource/metadata" +) + +const ( + apiVersion = "computeMetadata/v1/" + metadataPath = apiVersion + "instance/" + userdataPath = apiVersion + "instance/attributes/user-data" +) + +type metadataService struct { + metadata.MetadataService +} + +func NewDatasource(root string) *metadataService { + return &metadataService{metadata.NewDatasource(root, apiVersion, userdataPath, metadataPath, http.Header{"Metadata-Flavor": {"Google"}})} +} + +func (ms metadataService) FetchMetadata() (datasource.Metadata, error) { + public, err := ms.fetchIP("network-interfaces/0/access-configs/0/external-ip") + if err != nil { + return datasource.Metadata{}, err + } + local, err := ms.fetchIP("network-interfaces/0/ip") + if err != nil { + return datasource.Metadata{}, err + } + hostname, err := ms.fetchString("hostname") + if err != nil { + return datasource.Metadata{}, err + } + + return datasource.Metadata{ + PublicIPv4: public, + PrivateIPv4: local, + Hostname: hostname, + }, nil +} + +func (ms metadataService) Type() string { + return "gce-metadata-service" +} + +func (ms metadataService) fetchString(key string) (string, error) { + data, err := ms.FetchData(ms.MetadataUrl() + key) + if err != nil { + return "", err + } + + return string(data), nil +} + +func (ms metadataService) fetchIP(key string) (net.IP, error) { + str, err := ms.fetchString(key) + if err != nil { + return nil, err + } + + if str == "" { + return nil, nil + } + + if ip := net.ParseIP(str); ip != nil { + return ip, nil + } else { + return nil, fmt.Errorf("couldn't parse %q as IP address", str) + } +} diff --git a/datasource/metadata/gce/metadata_test.go b/datasource/metadata/gce/metadata_test.go new file mode 100644 index 00000000..0cc59f1d --- /dev/null +++ b/datasource/metadata/gce/metadata_test.go @@ -0,0 +1,99 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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" + "net" + "reflect" + "testing" + + "github.com/coreos/coreos-cloudinit/datasource" + "github.com/coreos/coreos-cloudinit/datasource/metadata" + "github.com/coreos/coreos-cloudinit/datasource/metadata/test" + "github.com/coreos/coreos-cloudinit/pkg" +) + +func TestType(t *testing.T) { + want := "gce-metadata-service" + if kind := (metadataService{}).Type(); kind != want { + t.Fatalf("bad type: want %q, got %q", want, kind) + } +} + +func TestFetchMetadata(t *testing.T) { + for _, tt := range []struct { + root string + metadataPath string + resources map[string]string + expect datasource.Metadata + clientErr error + expectErr error + }{ + { + root: "/", + metadataPath: "computeMetadata/v1/instance/", + resources: map[string]string{}, + }, + { + root: "/", + metadataPath: "computeMetadata/v1/instance/", + resources: map[string]string{ + "/computeMetadata/v1/instance/hostname": "host", + }, + expect: datasource.Metadata{ + Hostname: "host", + }, + }, + { + root: "/", + metadataPath: "computeMetadata/v1/instance/", + resources: map[string]string{ + "/computeMetadata/v1/instance/hostname": "host", + "/computeMetadata/v1/instance/network-interfaces/0/ip": "1.2.3.4", + "/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip": "5.6.7.8", + }, + expect: datasource.Metadata{ + Hostname: "host", + PrivateIPv4: net.ParseIP("1.2.3.4"), + PublicIPv4: net.ParseIP("5.6.7.8"), + }, + }, + { + clientErr: pkg.ErrTimeout{Err: fmt.Errorf("test error")}, + expectErr: pkg.ErrTimeout{Err: fmt.Errorf("test error")}, + }, + } { + service := &metadataService{metadata.MetadataService{ + Root: tt.root, + Client: &test.HttpClient{Resources: tt.resources, Err: tt.clientErr}, + MetadataPath: tt.metadataPath, + }} + metadata, err := service.FetchMetadata() + if Error(err) != Error(tt.expectErr) { + t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err) + } + if !reflect.DeepEqual(tt.expect, metadata) { + t.Fatalf("bad fetch (%q): want %#v, got %#v", tt.resources, tt.expect, metadata) + } + } +} + +func Error(err error) string { + if err != nil { + return err.Error() + } + return "" +} diff --git a/datasource/metadata/metadata.go b/datasource/metadata/metadata.go index 2baa4f17..9d52e933 100644 --- a/datasource/metadata/metadata.go +++ b/datasource/metadata/metadata.go @@ -15,6 +15,7 @@ package metadata import ( + "net/http" "strings" "github.com/coreos/coreos-cloudinit/pkg" @@ -28,11 +29,11 @@ type MetadataService struct { MetadataPath string } -func NewDatasource(root, apiVersion, userdataPath, metadataPath string) MetadataService { +func NewDatasource(root, apiVersion, userdataPath, metadataPath string, header http.Header) MetadataService { if !strings.HasSuffix(root, "/") { root += "/" } - return MetadataService{root, pkg.NewHttpClient(), apiVersion, userdataPath, metadataPath} + return MetadataService{root, pkg.NewHttpClientHeader(header), apiVersion, userdataPath, metadataPath} } func (ms MetadataService) IsAvailable() bool { diff --git a/datasource/metadata/metadata_test.go b/datasource/metadata/metadata_test.go index 9d6b4b29..5d2258f0 100644 --- a/datasource/metadata/metadata_test.go +++ b/datasource/metadata/metadata_test.go @@ -170,7 +170,7 @@ func TestNewDatasource(t *testing.T) { expectRoot: "http://169.254.169.254/", }, } { - service := NewDatasource(tt.root, "", "", "") + service := NewDatasource(tt.root, "", "", "", nil) if service.Root != tt.expectRoot { t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.Root) } diff --git a/datasource/metadata/packet/metadata.go b/datasource/metadata/packet/metadata.go index 3c7d499d..19d3931a 100644 --- a/datasource/metadata/packet/metadata.go +++ b/datasource/metadata/packet/metadata.go @@ -62,7 +62,7 @@ type metadataService struct { } func NewDatasource(root string) *metadataService { - return &metadataService{MetadataService: metadata.NewDatasource(root, apiVersion, userdataUrl, metadataPath)} + return &metadataService{MetadataService: metadata.NewDatasource(root, apiVersion, userdataUrl, metadataPath, nil)} } func (ms *metadataService) FetchMetadata() (metadata datasource.Metadata, err error) { diff --git a/pkg/http_client.go b/pkg/http_client.go index c4fb8032..79556a88 100644 --- a/pkg/http_client.go +++ b/pkg/http_client.go @@ -62,8 +62,8 @@ type HttpClient struct { // Maximum number of connection retries. Defaults to 15 MaxRetries int - // Whether or not to skip TLS verification. Defaults to false - SkipTLS bool + // Headers to add to the request. + Header http.Header client *http.Client } @@ -74,11 +74,15 @@ type Getter interface { } func NewHttpClient() *HttpClient { + return NewHttpClientHeader(nil) +} + +func NewHttpClientHeader(header http.Header) *HttpClient { hc := &HttpClient{ InitialBackoff: 50 * time.Millisecond, MaxBackoff: time.Second * 5, MaxRetries: 15, - SkipTLS: false, + Header: header, client: &http.Client{ Timeout: 10 * time.Second, }, @@ -139,7 +143,13 @@ func (h *HttpClient) GetRetry(rawurl string) ([]byte, error) { } func (h *HttpClient) Get(dataURL string) ([]byte, error) { - if resp, err := h.client.Get(dataURL); err == nil { + request, err := http.NewRequest("GET", dataURL, nil) + if err != nil { + return nil, err + } + + request.Header = h.Header + if resp, err := h.client.Do(request); err == nil { defer resp.Body.Close() switch resp.StatusCode / 100 { case HTTP_2xx: