diff --git a/Documentation/reservations.md b/Documentation/reservations.md index b70351629a..229fa4b78f 100644 --- a/Documentation/reservations.md +++ b/Documentation/reservations.md @@ -25,6 +25,16 @@ This shows that there is a single lease (`10.5.34.0/24`) which will expire in 85 The `"PublicIP"` value is how flannel knows to reuse this lease when restarted. This means that if the public IP changes, then the flannel subnet will change too. +In case a host is unable to renew its lease before the lease expires (e.g. a host takes a long time to restart and the timing lines up with when the lease would normally be renewed), flannel will then attempt to renew the last lease that it has saved in its subnet config file (which, unless specified, is located at `/var/run/flannel/subnet.env`) +```bash +cat /var/run/flannel/subnet.env +FLANNEL_NETWORK=10.5.0.0/16 +FLANNEL_SUBNET=10.5.34.1/24 +FLANNEL_MTU=1450 +FLANNEL_IPMASQ=false +``` +In this case, if flannel fails to retrieve an existing lease from etcd, it will attempt to renew lease specified in `FLANNEL_SUBNET` (`10.5.34.1/24`). It will only renew this lease if the subnet specified is valid for the current etcd network configuration otherwise it will allocate a new lease. + ## Reservations flannel also supports reservations for the subnet assigned to a host. Reservations diff --git a/Documentation/running.md b/Documentation/running.md index fc8155e425..81e64ea036 100644 --- a/Documentation/running.md +++ b/Documentation/running.md @@ -46,6 +46,23 @@ FLANNEL_SUBNET=10.5.72.1/24 FLANNEL_MTU=1450 FLANNEL_IPMASQ=false ``` +Each time flannel is restarted, it will attempt to access the `FLANNEL_SUBNET` value written in this subnet config file. This prevents each host from needing to update its network information in case a host is unable to renew its lease before it expires (e.g. a host was restarting during the time flannel would normally renew its lease). + +The `FLANNEL_SUBNET` value is also only used if it is valid for the etcd network config. For instance, a `FLANNEL_SUBNET` value of `10.5.72.1/24` will not be used if the etcd network value is set to `10.6.0.0/16` since it is not within that network range. + +Subnet config value is `10.5.72.1/24` +```bash +cat /var/run/flannel/subnet.env +FLANNEL_NETWORK=10.5.0.0/16 +FLANNEL_SUBNET=10.5.72.1/24 +FLANNEL_MTU=1450 +FLANNEL_IPMASQ=false +``` +etcd network value is `10.6.0.0/16`. Since `10.5.72.1/24` is outside of this network, a new lease will be allocated. +```bash +etcdctl get /coreos.com/network/config +{ "Network": "10.6.0.0/16", "Backend": {"Type": "vxlan"}} +``` ## Interface selection diff --git a/glide.lock b/glide.lock index f6492f9906..9dc9bebd3a 100644 --- a/glide.lock +++ b/glide.lock @@ -104,6 +104,8 @@ imports: version: 392c28fe23e1c45ddba891b0320b3b5df220beea - name: github.com/jmespath/go-jmespath version: 3433f3ea46d9f8019119e7dd41274e112a2359a9 +- name: github.com/joho/godotenv + version: 726cc8b906e3d31c70a9671c90a13716a8d3f50d - name: github.com/jonboulle/clockwork version: 2eee05ed794112d45db504eb05aa693efd2b8b09 - name: github.com/juju/ratelimit diff --git a/glide.yaml b/glide.yaml index 94c05bd702..da7dcf09c1 100644 --- a/glide.yaml +++ b/glide.yaml @@ -58,3 +58,5 @@ import: - pkg/runtime - pkg/util/wait - pkg/watch +- package: github.com/joho/godotenv + version: v1.1 diff --git a/main.go b/main.go index efb75cc0bd..691b6aae4e 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,8 @@ import ( "time" + "github.com/joho/godotenv" + // Backends need to be imported for their init() to get executed and them to register "github.com/coreos/flannel/backend" _ "github.com/coreos/flannel/backend/alivpc" @@ -118,7 +120,10 @@ func newSubnetManager() (subnet.Manager, error) { Password: opts.etcdPassword, } - return etcdv2.NewLocalManager(cfg) + // Attempt to renew the lease for the subnet specified in the subnetFile + prevSubnet := ReadSubnetFromSubnetFile(opts.subnetFile) + + return etcdv2.NewLocalManager(cfg, prevSubnet) } func main() { @@ -456,3 +461,19 @@ func mustRunHealthz() { panic(err) } } + +func ReadSubnetFromSubnetFile(path string) ip.IP4Net { + var prevSubnet ip.IP4Net + if _, err := os.Stat(path); !os.IsNotExist(err) { + prevSubnetVals, err := godotenv.Read(path) + if err != nil { + log.Errorf("Couldn't fetch previous subnet from subnet file at %s: %s", path, err) + } else if prevSubnetString, ok := prevSubnetVals["FLANNEL_SUBNET"]; ok { + err = prevSubnet.UnmarshalJSON([]byte(prevSubnetString)) + if err != nil { + log.Errorf("Couldn't parse previous subnet from subnet file at %s: %s", path, err) + } + } + } + return prevSubnet +} diff --git a/pkg/ip/ipnet.go b/pkg/ip/ipnet.go index 8b8e889a7a..ae75c86e8b 100644 --- a/pkg/ip/ipnet.go +++ b/pkg/ip/ipnet.go @@ -159,6 +159,10 @@ func (n IP4Net) Contains(ip IP4) bool { return (uint32(n.IP) & n.Mask()) == (uint32(ip) & n.Mask()) } +func (n IP4Net) Empty() bool { + return n.IP == IP4(0) && n.PrefixLen == uint(0) +} + // json.Marshaler impl func (n IP4Net) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`"%s"`, n)), nil diff --git a/subnet/etcdv2/local_manager.go b/subnet/etcdv2/local_manager.go index b52b42bb31..04f78a84f7 100644 --- a/subnet/etcdv2/local_manager.go +++ b/subnet/etcdv2/local_manager.go @@ -33,7 +33,8 @@ const ( ) type LocalManager struct { - registry Registry + registry Registry + previousSubnet ip.IP4Net } type watchCursor struct { @@ -68,17 +69,18 @@ func (c watchCursor) String() string { return strconv.FormatUint(c.index, 10) } -func NewLocalManager(config *EtcdConfig) (Manager, error) { +func NewLocalManager(config *EtcdConfig, prevSubnet ip.IP4Net) (Manager, error) { r, err := newEtcdSubnetRegistry(config, nil) if err != nil { return nil, err } - return newLocalManager(r), nil + return newLocalManager(r, prevSubnet), nil } -func newLocalManager(r Registry) Manager { +func newLocalManager(r Registry, prevSubnet ip.IP4Net) Manager { return &LocalManager{ - registry: r, + registry: r, + previousSubnet: prevSubnet, } } @@ -155,10 +157,24 @@ func (m *LocalManager) tryAcquireLease(ctx context.Context, config *Config, extI } } - // no existing match, grab a new one - sn, err := m.allocateSubnet(config, leases) - if err != nil { - return nil, err + // no existing match, check if there was a previous subnet to use + var sn ip.IP4Net + // Check if the previous subnet is a part of the network and of the right subnet length + if !m.previousSubnet.Empty() && isSubnetConfigCompat(config, m.previousSubnet) { + // use previous subnet + log.Infof("Found previously leased subnet (%v), reusing", m.previousSubnet) + sn = m.previousSubnet + } else { + // Create error message for info + if !m.previousSubnet.Empty() { + log.Errorf("Found previously leased subnet (%v) that is not compatible with the Etcd network config, ignoring", m.previousSubnet) + } + + // no existing match, grab a new one + sn, err = m.allocateSubnet(config, leases) + if err != nil { + return nil, err + } } exp, err := m.registry.createSubnet(ctx, sn, attrs, subnetTTL) diff --git a/subnet/etcdv2/mock_subnet.go b/subnet/etcdv2/mock_subnet.go index 6eaf7d374e..4b50419add 100644 --- a/subnet/etcdv2/mock_subnet.go +++ b/subnet/etcdv2/mock_subnet.go @@ -15,9 +15,14 @@ package etcdv2 import ( + "github.com/coreos/flannel/pkg/ip" "github.com/coreos/flannel/subnet" ) func NewMockManager(registry *MockSubnetRegistry) subnet.Manager { - return newLocalManager(registry) + return newLocalManager(registry, ip.IP4Net{}) +} + +func NewMockManagerWithSubnet(registry *MockSubnetRegistry, sn ip.IP4Net) subnet.Manager { + return newLocalManager(registry, sn) } diff --git a/subnet/etcdv2/subnet_test.go b/subnet/etcdv2/subnet_test.go index 9b2218b0ea..c6fb23d4ee 100644 --- a/subnet/etcdv2/subnet_test.go +++ b/subnet/etcdv2/subnet_test.go @@ -76,6 +76,30 @@ func TestAcquireLease(t *testing.T) { if !l.Subnet.Equal(l2.Subnet) { t.Fatalf("AcquireLease did not reuse subnet; expected %v, got %v", l.Subnet, l2.Subnet) } + + // Test if a previous subnet will be used + msr2 := newDummyRegistry() + prevSubnet := ip.IP4Net{ip.MustParseIP4("10.3.6.0"), 24} + sm2 := NewMockManagerWithSubnet(msr2, prevSubnet) + prev, err := sm2.AcquireLease(context.Background(), &attrs) + if err != nil { + t.Fatal("AcquireLease failed: ", err) + } + if !prev.Subnet.Equal(prevSubnet) { + t.Fatalf("AcquireLease did not reuse subnet from previous run; expected %v, got %v", prevSubnet, prev.Subnet) + } + + // Test that a previous subnet will not be used if it does not match the registry config + msr3 := newDummyRegistry() + invalidSubnet := ip.IP4Net{ip.MustParseIP4("10.4.1.0"), 24} + sm3 := NewMockManagerWithSubnet(msr3, invalidSubnet) + l3, err := sm3.AcquireLease(context.Background(), &attrs) + if err != nil { + t.Fatal("AcquireLease failed: ", err) + } + if l3.Subnet.Equal(invalidSubnet) { + t.Fatalf("AcquireLease reused invalid subnet from previous run; reused %v", l3.Subnet) + } } func TestConfigChanged(t *testing.T) { diff --git a/vendor/github.com/joho/godotenv/.gitignore b/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000000..e43b0f9889 --- /dev/null +++ b/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/vendor/github.com/joho/godotenv/LICENCE b/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000000..e7ddd51be9 --- /dev/null +++ b/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/github.com/joho/godotenv/README.md b/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000000..05c47e6f5e --- /dev/null +++ b/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,127 @@ +# GoDotEnv [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78 "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) + +A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file) + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc) or as a bin command. + +There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "github.com/joho/godotenv" + "log" + "os" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +_ = godotenv.Load("somerandomfile") +_ = godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +## Contributing + +Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases. + +*code changes without tests will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## CI + +Linux: [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78/m "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv) + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](http://whoisjohnbarton.com) based off the tests/fixtures in the original library. diff --git a/vendor/github.com/joho/godotenv/autoload/autoload.go b/vendor/github.com/joho/godotenv/autoload/autoload.go new file mode 100644 index 0000000000..fbcd2bdf8e --- /dev/null +++ b/vendor/github.com/joho/godotenv/autoload/autoload.go @@ -0,0 +1,15 @@ +package autoload + +/* + You can just read the .env file on import just by doing + + import _ "github.com/joho/godotenv/autoload" + + And bob's your mother's brother +*/ + +import "github.com/joho/godotenv" + +func init() { + godotenv.Load() +} diff --git a/vendor/github.com/joho/godotenv/cmd/godotenv/cmd.go b/vendor/github.com/joho/godotenv/cmd/godotenv/cmd.go new file mode 100644 index 0000000000..04a9f6497f --- /dev/null +++ b/vendor/github.com/joho/godotenv/cmd/godotenv/cmd.go @@ -0,0 +1,54 @@ +package main + +import ( + "flag" + "fmt" + "log" + + "strings" + + "github.com/joho/godotenv" +) + +func main() { + var showHelp bool + flag.BoolVar(&showHelp, "h", false, "show help") + var rawEnvFilenames string + flag.StringVar(&rawEnvFilenames, "f", "", "comma separated paths to .env files") + + flag.Parse() + + usage := ` +Run a process with a env setup from a .env file + +godotenv [-f ENV_FILE_PATHS] COMMAND_ARGS + +ENV_FILE_PATHS: comma separated paths to .env files +COMMAND_ARGS: command and args you want to run + +example + godotenv -f /path/to/something/.env,/another/path/.env fortune +` + // if no args or -h flag + // print usage and return + args := flag.Args() + if showHelp || len(args) == 0 { + fmt.Println(usage) + return + } + + // load env + var envFilenames []string + if rawEnvFilenames != "" { + envFilenames = strings.Split(rawEnvFilenames, ",") + } + + // take rest of args and "exec" them + cmd := args[0] + cmdArgs := args[1:] + + err := godotenv.Exec(envFilenames, cmd, cmdArgs) + if err != nil { + log.Fatal(err) + } +} diff --git a/vendor/github.com/joho/godotenv/fixtures/equals.env b/vendor/github.com/joho/godotenv/fixtures/equals.env new file mode 100644 index 0000000000..594c5328ae --- /dev/null +++ b/vendor/github.com/joho/godotenv/fixtures/equals.env @@ -0,0 +1,2 @@ +export OPTION_A='postgres://localhost:5432/database?sslmode=disable' + diff --git a/vendor/github.com/joho/godotenv/fixtures/exported.env b/vendor/github.com/joho/godotenv/fixtures/exported.env new file mode 100644 index 0000000000..5821377c76 --- /dev/null +++ b/vendor/github.com/joho/godotenv/fixtures/exported.env @@ -0,0 +1,2 @@ +export OPTION_A=2 +export OPTION_B='\n' diff --git a/vendor/github.com/joho/godotenv/fixtures/invalid1.env b/vendor/github.com/joho/godotenv/fixtures/invalid1.env new file mode 100644 index 0000000000..38f7e0e8bf --- /dev/null +++ b/vendor/github.com/joho/godotenv/fixtures/invalid1.env @@ -0,0 +1,2 @@ +INVALID LINE +foo=bar diff --git a/vendor/github.com/joho/godotenv/fixtures/plain.env b/vendor/github.com/joho/godotenv/fixtures/plain.env new file mode 100644 index 0000000000..c983b06416 --- /dev/null +++ b/vendor/github.com/joho/godotenv/fixtures/plain.env @@ -0,0 +1,5 @@ +OPTION_A=1 +OPTION_B=2 +OPTION_C= 3 +OPTION_D =4 +OPTION_E = 5 diff --git a/vendor/github.com/joho/godotenv/fixtures/quoted.env b/vendor/github.com/joho/godotenv/fixtures/quoted.env new file mode 100644 index 0000000000..a03ce249bf --- /dev/null +++ b/vendor/github.com/joho/godotenv/fixtures/quoted.env @@ -0,0 +1,8 @@ +OPTION_A='1' +OPTION_B='2' +OPTION_C='' +OPTION_D='\n' +OPTION_E="1" +OPTION_F="2" +OPTION_G="" +OPTION_H="\n" diff --git a/vendor/github.com/joho/godotenv/godotenv.go b/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000000..a098dd2156 --- /dev/null +++ b/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,235 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the github page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be avaiable through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bufio" + "errors" + "os" + "os/exec" + "strings" +) + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main) +// +// If you call Load without any args it will default to loading .env in the current path +// +// You can otherwise tell it which files to load (there can be more than one) like +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main) +// +// If you call Overload without any args it will default to loading .env in the current path +// +// You can otherwise tell it which files to load (there can be more than one) like +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run() +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string) error { + Load(filenames...) + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + for key, value := range envMap { + if os.Getenv(key) == "" || overload { + os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + envMap = make(map[string]string) + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if err = scanner.Err(); err != nil { + return + } + + for _, fullLine := range lines { + if !isIgnoredLine(fullLine) { + var key, value string + key, value, err = parseLine(fullLine) + + if err != nil { + return + } + envMap[key] = value + } + } + return +} + +func parseLine(line string) (key string, value string, err error) { + if len(line) == 0 { + err = errors.New("zero length string") + return + } + + // ditch the comments (but keep quoted hashes) + if strings.Contains(line, "#") { + segmentsBetweenHashes := strings.Split(line, "#") + quotesAreOpen := false + var segmentsToKeep []string + for _, segment := range segmentsBetweenHashes { + if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 { + if quotesAreOpen { + quotesAreOpen = false + segmentsToKeep = append(segmentsToKeep, segment) + } else { + quotesAreOpen = true + } + } + + if len(segmentsToKeep) == 0 || quotesAreOpen { + segmentsToKeep = append(segmentsToKeep, segment) + } + } + + line = strings.Join(segmentsToKeep, "#") + } + + // now split key from value + splitString := strings.SplitN(line, "=", 2) + + if len(splitString) != 2 { + // try yaml mode! + splitString = strings.SplitN(line, ":", 2) + } + + if len(splitString) != 2 { + err = errors.New("Can't separate key from value") + return + } + + // Parse the key + key = splitString[0] + if strings.HasPrefix(key, "export") { + key = strings.TrimPrefix(key, "export") + } + key = strings.Trim(key, " ") + + // Parse the value + value = splitString[1] + // trim + value = strings.Trim(value, " ") + + // check if we've got quoted values + if strings.Count(value, "\"") == 2 || strings.Count(value, "'") == 2 { + // pull the quotes off the edges + value = strings.Trim(value, "\"'") + + // expand quotes + value = strings.Replace(value, "\\\"", "\"", -1) + // expand newlines + value = strings.Replace(value, "\\n", "\n", -1) + } + + return +} + +func isIgnoredLine(line string) bool { + trimmedLine := strings.Trim(line, " \n\t") + return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") +} diff --git a/vendor/github.com/joho/godotenv/godotenv_test.go b/vendor/github.com/joho/godotenv/godotenv_test.go new file mode 100644 index 0000000000..2d26ee4865 --- /dev/null +++ b/vendor/github.com/joho/godotenv/godotenv_test.go @@ -0,0 +1,288 @@ +package godotenv + +import ( + "os" + "testing" +) + +var noopPresets = make(map[string]string) + +func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) { + key, value, _ := parseLine(rawEnvLine) + if key != expectedKey || value != expectedValue { + t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value) + } +} + +func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, envFileName string, expectedValues map[string]string, presets map[string]string) { + // first up, clear the env + os.Clearenv() + + for k, v := range presets { + os.Setenv(k, v) + } + + err := loader(envFileName) + if err != nil { + t.Fatalf("Error loading %v", envFileName) + } + + for k := range expectedValues { + envValue := os.Getenv(k) + v := expectedValues[k] + if envValue != v { + t.Errorf("Mismatch for key '%v': expected '%v' got '%v'", k, v, envValue) + } + } +} + +func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) { + err := Load() + pathError := err.(*os.PathError) + if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" { + t.Errorf("Didn't try and open .env by default") + } +} + +func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) { + err := Overload() + pathError := err.(*os.PathError) + if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" { + t.Errorf("Didn't try and open .env by default") + } +} + +func TestLoadFileNotFound(t *testing.T) { + err := Load("somefilethatwillneverexistever.env") + if err == nil { + t.Error("File wasn't found but Load didn't return an error") + } +} + +func TestOverloadFileNotFound(t *testing.T) { + err := Overload("somefilethatwillneverexistever.env") + if err == nil { + t.Error("File wasn't found but Overload didn't return an error") + } +} + +func TestReadPlainEnv(t *testing.T) { + envFileName := "fixtures/plain.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "2", + "OPTION_C": "3", + "OPTION_D": "4", + "OPTION_E": "5", + } + + envMap, err := Read(envFileName) + if err != nil { + t.Error("Error reading file") + } + + if len(envMap) != len(expectedValues) { + t.Error("Didn't get the right size map back") + } + + for key, value := range expectedValues { + if envMap[key] != value { + t.Error("Read got one of the keys wrong") + } + } +} + +func TestLoadDoesNotOverride(t *testing.T) { + envFileName := "fixtures/plain.env" + + // ensure NO overload + presets := map[string]string{ + "OPTION_A": "do_not_override", + } + + expectedValues := map[string]string{ + "OPTION_A": "do_not_override", + } + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets) +} + +func TestOveroadDoesOverride(t *testing.T) { + envFileName := "fixtures/plain.env" + + // ensure NO overload + presets := map[string]string{ + "OPTION_A": "do_not_override", + } + + expectedValues := map[string]string{ + "OPTION_A": "1", + } + loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets) +} + +func TestLoadPlainEnv(t *testing.T) { + envFileName := "fixtures/plain.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "2", + "OPTION_C": "3", + "OPTION_D": "4", + "OPTION_E": "5", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestLoadExportedEnv(t *testing.T) { + envFileName := "fixtures/exported.env" + expectedValues := map[string]string{ + "OPTION_A": "2", + "OPTION_B": "\n", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestLoadEqualsEnv(t *testing.T) { + envFileName := "fixtures/equals.env" + expectedValues := map[string]string{ + "OPTION_A": "postgres://localhost:5432/database?sslmode=disable", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestLoadQuotedEnv(t *testing.T) { + envFileName := "fixtures/quoted.env" + expectedValues := map[string]string{ + "OPTION_A": "1", + "OPTION_B": "2", + "OPTION_C": "", + "OPTION_D": "\n", + "OPTION_E": "1", + "OPTION_F": "2", + "OPTION_G": "", + "OPTION_H": "\n", + } + + loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) +} + +func TestActualEnvVarsAreLeftAlone(t *testing.T) { + os.Clearenv() + os.Setenv("OPTION_A", "actualenv") + _ = Load("fixtures/plain.env") + + if os.Getenv("OPTION_A") != "actualenv" { + t.Error("An ENV var set earlier was overwritten") + } +} + +func TestParsing(t *testing.T) { + // unquoted values + parseAndCompare(t, "FOO=bar", "FOO", "bar") + + // parses values with spaces around equal sign + parseAndCompare(t, "FOO =bar", "FOO", "bar") + parseAndCompare(t, "FOO= bar", "FOO", "bar") + + // parses double quoted values + parseAndCompare(t, "FOO=\"bar\"", "FOO", "bar") + + // parses single quoted values + parseAndCompare(t, "FOO='bar'", "FOO", "bar") + + // parses escaped double quotes + parseAndCompare(t, "FOO=escaped\\\"bar\"", "FOO", "escaped\"bar") + + // parses yaml style options + parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1") + + // parses export keyword + parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2") + parseAndCompare(t, "export OPTION_B='\\n'", "OPTION_B", "\n") + + // it 'expands newlines in quoted strings' do + // expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz") + parseAndCompare(t, "FOO=\"bar\\nbaz\"", "FOO", "bar\nbaz") + + // it 'parses varibales with "." in the name' do + // expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar') + parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar") + + // it 'parses varibales with several "=" in the value' do + // expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=') + parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=") + + // it 'strips unquoted values' do + // expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar ' + parseAndCompare(t, "FOO=bar ", "FOO", "bar") + + // it 'ignores inline comments' do + // expect(env("foo=bar # this is foo")).to eql('foo' => 'bar') + parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar") + + // it 'allows # in quoted value' do + // expect(env('foo="bar#baz" # comment')).to eql('foo' => 'bar#baz') + parseAndCompare(t, "FOO=\"bar#baz\" # comment", "FOO", "bar#baz") + parseAndCompare(t, "FOO='bar#baz' # comment", "FOO", "bar#baz") + parseAndCompare(t, "FOO=\"bar#baz#bang\" # comment", "FOO", "bar#baz#bang") + + // it 'parses # in quoted values' do + // expect(env('foo="ba#r"')).to eql('foo' => 'ba#r') + // expect(env("foo='ba#r'")).to eql('foo' => 'ba#r') + parseAndCompare(t, "FOO=\"ba#r\"", "FOO", "ba#r") + parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r") + + // it 'throws an error if line format is incorrect' do + // expect{env('lol$wut')}.to raise_error(Dotenv::FormatError) + badlyFormattedLine := "lol$wut" + _, _, err := parseLine(badlyFormattedLine) + if err == nil { + t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine) + } +} + +func TestLinesToIgnore(t *testing.T) { + // it 'ignores empty lines' do + // expect(env("\n \t \nfoo=bar\n \nfizz=buzz")).to eql('foo' => 'bar', 'fizz' => 'buzz') + if !isIgnoredLine("\n") { + t.Error("Line with nothing but line break wasn't ignored") + } + + if !isIgnoredLine("\t\t ") { + t.Error("Line full of whitespace wasn't ignored") + } + + // it 'ignores comment lines' do + // expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql('foo' => 'bar') + if !isIgnoredLine("# comment") { + t.Error("Comment wasn't ignored") + } + + if !isIgnoredLine("\t#comment") { + t.Error("Indented comment wasn't ignored") + } + + // make sure we're not getting false positives + if isIgnoredLine("export OPTION_B='\\n'") { + t.Error("ignoring a perfectly valid line to parse") + } +} + +func TestErrorReadDirectory(t *testing.T) { + envFileName := "fixtures/" + envMap, err := Read(envFileName) + + if err == nil { + t.Errorf("Expected error, got %v", envMap) + } +} + +func TestErrorParsing(t *testing.T) { + envFileName := "fixtures/invalid1.env" + envMap, err := Read(envFileName) + if err == nil { + t.Errorf("Expected error, got %v", envMap) + } +} diff --git a/vendor/github.com/joho/godotenv/wercker.yml b/vendor/github.com/joho/godotenv/wercker.yml new file mode 100644 index 0000000000..c716ac926a --- /dev/null +++ b/vendor/github.com/joho/godotenv/wercker.yml @@ -0,0 +1 @@ +box: pjvds/golang