diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa3d7cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/* +terraform-provider-fleet + diff --git a/README.md b/README.md index a9c25be..1c5e0f5 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,41 @@ A plugin for Terraform enabling it to manipulate ## Usage +This terraform plugin supports basic connections to the ETCD endpoint, +the HTTP API endpoint, and over SSH + +There is minimal configuration currently supported so the ETCD and API clients +are attempting to connect directly without SSL + +The configuration value 'driver' defaults to 'tunnel' but can be configured with: + * etcd + * api + +EX: + +``` +provider "fleet" { + driver = "etcd" + endpoint = "http://192.168.0.1:4001" + // etcd_key_prefix can be ommited to use the default value + etcd_key_prefix = "/_some/_weird/etcd/prefix" + // connection_retries defaults to 12 + connection_retries = 9000 +} +``` + +``` +provider "fleet" { + driver = "api" + endpoint = "http://192.168.0.1:8080" +} +``` + There is only one resource: `fleet_unit`. Here is the first example from [the Fleet introduction][3], transcribed to Terraform: provider "fleet" { - tunnel_address = "IP_OR_HOSTNAME_OF_A_COREOS_HOST" + endpoint = "IP_OR_HOSTNAME_OF_A_COREOS_HOST" } resource "fleet_unit" "myapp" { diff --git a/provider.go b/provider.go index 74a659d..a615fd4 100644 --- a/provider.go +++ b/provider.go @@ -6,6 +6,8 @@ import ( "net/url" "strings" "time" + "fmt" + "log" "github.com/coreos/fleet/client" "github.com/coreos/fleet/pkg" @@ -13,8 +15,23 @@ import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" + + "github.com/coreos/fleet/version" + "github.com/coreos/fleet/registry" + + etcd "github.com/coreos/fleet/Godeps/_workspace/src/github.com/coreos/etcd/client" ) +const oldVersionWarning = `#################################################################### +WARNING: The linked against fleet go lib (%s) is older than the latest +registered version of fleet found in the cluster (%s). You are strongly +recommended to upgrade the linked fleet go lib and rebuild to prevent +incompatibility issues. +#################################################################### +` + +const defaultTimeout = time.Second*10 + // retry wraps a function with retry logic. Only errors containing "timed out" // will be retried. (authentication errors and other stuff should fail // immediately) @@ -31,14 +48,67 @@ func retry(f func() (interface{}, error), maxRetries int) (interface{}, error) { return result, err } -// getAPI returns an API to Fleet. -func getAPI(hostAddr string, maxRetries int) (client.API, error) { - if hostAddr == "" { - return nullAPI{}, nil +func checkVersion(cReg registry.ClusterRegistry) (string, bool) { + fv := version.SemVersion + lv, err := cReg.LatestDaemonVersion() + if err != nil { + log.Fatal("error attempting to check latest fleet version in Registry: %v", err) + } else if lv != nil && fv.LessThan(*lv) { + return fmt.Sprintf(oldVersionWarning, fv.String(), lv.String()), false + } + return "", true +} + +func getHTTPClient(driverEndpoint string) (client.API, error) { + log.Printf("Using API connection for requests") + + endpoint, err := url.Parse(driverEndpoint) + + if err != nil { + return nil, err } + trans := pkg.LoggingHTTPTransport{ + Transport: http.Transport{}, + } + + httpClient := http.Client{ + Transport: &trans, + } + + return client.NewHTTPClient(&httpClient, *endpoint) +} + +func getETCDClient(driverEndpoint string, etcdKeyPrefix string) (client.API, error) { + log.Printf("Using ETCD connection for requests") + + trans := &http.Transport{} + + eCfg := etcd.Config{ + Endpoints: strings.Split(driverEndpoint, ","), + Transport: trans, + } + + eClient, err := etcd.New(eCfg) + if err != nil { + return nil, err + } + + keysAPI := etcd.NewKeysAPI(eClient) + reg := registry.NewEtcdRegistry(keysAPI, etcdKeyPrefix, defaultTimeout) + + if msg, ok := checkVersion(reg); !ok { + log.Printf(msg) + } + + return &client.RegistryClient{Registry: reg}, nil +} + +func getTunnelClient(driverEndpoint string, tunnelEndpoint string, maxRetries int) (client.API, error) { + log.Printf("Using Fleet Tunnel connection for requests") + getSSHClient := func() (interface{}, error) { - return ssh.NewSSHClient("core", hostAddr, nil, false, time.Second*10) + return ssh.NewSSHClient("core", driverEndpoint, nil, false, defaultTimeout) } result, err := retry(getSSHClient, maxRetries) @@ -48,10 +118,19 @@ func getAPI(hostAddr string, maxRetries int) (client.API, error) { sshClient := result.(*ssh.SSHForwardingClient) dial := func(string, string) (net.Conn, error) { - cmd := "fleetctl fd-forward /var/run/fleet.sock" + cmd := fmt.Sprintf("fleetctl fd-forward %s", tunnelEndpoint) return ssh.DialCommand(sshClient, cmd) } + // This is needed to fake out the client - it isn't used + // since we're overloading the dial method on the transport + // but the client complains if it isn't set + fakeHTTPEndpoint, err := url.Parse("http://domain-sock") + + if err != nil { + return nil, err + } + trans := pkg.LoggingHTTPTransport{ Transport: http.Transport{ Dial: dial, @@ -62,11 +141,28 @@ func getAPI(hostAddr string, maxRetries int) (client.API, error) { Transport: &trans, } - // since dial() ignores the endpoint, we just need something here that - // won't make the HTTP client complain. - endpoint, err := url.Parse("http://domain-sock") + return client.NewHTTPClient(&httpClient, *fakeHTTPEndpoint) +} - return client.NewHTTPClient(&httpClient, *endpoint) + +// getAPI returns an API to Fleet. +func getAPI(driver string, driverEndpoint string, maxRetries int, etcdKeyPrefix string, tunnel string) (client.API, error) { + + switch strings.ToLower(driver) { + case "api": + return getHTTPClient(driverEndpoint) + case "etcd": + return getETCDClient(driverEndpoint, etcdKeyPrefix) + case "tunnel": + if len(driverEndpoint) > 0 { + return getTunnelClient(driverEndpoint, tunnel, maxRetries) + } + fallthrough + case "null": + fallthrough + default: + return nullAPI{}, nil + } } // Provider returns the ResourceProvider implemented by this package. Serve @@ -74,15 +170,34 @@ func getAPI(hostAddr string, maxRetries int) (client.API, error) { func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ - "tunnel_address": &schema.Schema{ + "driver": &schema.Schema{ Type: schema.TypeString, - Required: true, + Optional: true, + Default: "tunnel", + Description: "Driver to use to connect to Fleet. Can be tunnel or api.", + }, + "endpoint": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Endpoint for Fleet.", }, "connection_retries": &schema.Schema{ Type: schema.TypeInt, Optional: true, Default: 12, }, + "etcd_key_prefix": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/_coreos.com/fleet/", + Description: "EtcdKeyPrefix to use for fleet", + }, + "tunnel": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/var/run/fleet.sock", + Description: "Tunnel address to use. Defaults to /var/run/fleet.sock", + }, }, ResourcesMap: map[string]*schema.Resource{ "fleet_unit": resourceUnit(), @@ -92,7 +207,10 @@ func Provider() terraform.ResourceProvider { } func providerConfigure(d *schema.ResourceData) (interface{}, error) { - addr := d.Get("tunnel_address").(string) retries := d.Get("connection_retries").(int) - return getAPI(addr, retries) + driver := d.Get("driver").(string) + endpoint := d.Get("endpoint").(string) + etcKeyPrefix := d.Get("etcd_key_prefix").(string) + tunnel := d.Get("tunnel").(string) + return getAPI(driver, endpoint, retries, etcKeyPrefix, tunnel) } diff --git a/provider_test.go b/provider_test.go index 581c648..253ef18 100644 --- a/provider_test.go +++ b/provider_test.go @@ -58,7 +58,25 @@ func TestGetAPI(test *testing.T) { // when the address is an empty string, we get a nullAPI var api client.API - api, err := getAPI("", 1) + api, err := getAPI("null", "endpoint", 1, "etcd_prefix", "tunnel_address") + + if err != nil { + test.Fatal(err) + } + + switch api.(type) { + case nullAPI: + // pass! + default: + test.Errorf("didn't get nullAPI, got %s instead", api) + } +} + +func TestGetAPIEmptyEndpoint(test *testing.T) { + // when the address is an empty string, we get a nullAPI + var api client.API + + api, err := getAPI("tunnel", "", 1, "etcd_prefix", "tunnel_address") if err != nil { test.Fatal(err)