Skip to content

Commit

Permalink
Fixes cloudfoundry-community#29 allow setting environment variables i…
Browse files Browse the repository at this point in the history
…ncluding CF_DIAL_TIMEOUT

**Why**

Using the `cf` resource in a pipeline which has a high latency
connection to the CloudFoundry instance fails and the solution is to set
the `CF_DIAL_TIMEOUT` to a higher value (15s is recommended). For
instance, this occurs running a Concourse pipeline in AWS Sydney and
targeting PWS in the US.
By extension, there is no mechanism for setting any of the `CF_`
environment variables (or other environment variables if they were
needed).

**How**

The design of the solution has taken a general 'allow shell environment
setting' approach as opposed to a 'create individual config params for
all CF_ switches'. This is to remove the burden of keeping this resource
in sync with the `cf` command line tool while giving the benefit of easy
environment config.

The solution adds a `CommandEnvironmentVariables` struct to the
`Source` struct, allowing a map of environment key-value pairs to be
added in to the Source Configuration YAML.

A new CfEnvironment struct is used to hold the internal model of the
environment variables. There are constructors to pre-populated with
environment variables or not. Both constructors pre-populate
`CF_COLOR=true` as before. The CloudFoundry struct is now composed of
a CfEnvironment.

The CfEnvironment uses a `map[string]string' but the JSON Unmarshalling
uses a `map[string]interface{}` for ease of type safety, so there is a
bit of `fmt.Sprintf("%v", v)` code for handling the conversion.

The `CommandEnvironmentVariables` is populated by the `main` during
JSON wrangling and then added to the `cloudFoundry`.
The environment is finally set up (as before) in the `CloudFoundry.cf`
method.

**Effects**

Any environment variables may be now set for the runtime environment of
the `cf` command via the Source Configuration YAML. Existing set
variable can be overridden. This is why a `map` was used internally
rather than `[]string` of `KEY=VAL` that os.Environ() returns and
`cmd.Env` takes.
  • Loading branch information
chrisp-cbh committed Jun 12, 2017
1 parent a4252bf commit 0ed3201
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 4 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ _testmain.go
/test.json
/*.tar.gz
/built*

# IDE
*.iml
.idea
.idea/**/*
*.swp
**/build

# OSX
**/.DS_Store
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Cloud Foundry deployment.
* `space`: *Required.* The space to push the application to.
* `skip_cert_check`: *Optional.* Check the validity of the CF SSL cert.
Defaults to `false`.
* `command_environment_variables`: *Optional.* Map of `CF_` environment variables.
See [cf-cli](https://docs.cloudfoundry.org/cf-cli/cf-help.html#environment-variables)
Note: `CF_COLOR` is set to `true` by default.

## Behaviour

Expand Down Expand Up @@ -67,4 +70,8 @@ resources:
organization: ORG
space: SPACE
skip_cert_check: false
command_environment_variables:
CF_COLOR: true
CF_DIAL_TIMEOUT: 15
CF_TRACE: true
```
1 change: 1 addition & 0 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Source struct {
Organization string `json:"organization"`
Space string `json:"space"`
SkipCertCheck bool `json:"skip_cert_check"`
CommandEnvironmentVariables map[string]interface{} `json:"command_environment_variables"`
}

type Version struct {
Expand Down
4 changes: 3 additions & 1 deletion out/assets/cf
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ echo $(basename $0) $*
echo
echo $PWD
echo
env
# env order needs to be predictable for testing as `gbytes.Say`
# in `integration_test` fast forwards through buffer contents
env | sort
66 changes: 66 additions & 0 deletions out/cf_environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package out

import (
"strings"
"os"
"fmt"
)

type CfEnvironment struct {
env map[string]string
}

func NewCfEnvironment() *CfEnvironment {
env := make(map[string]string)
env["CF_COLOR"]="true"

cfe := &CfEnvironment{env}

return cfe
}

func NewCfEnvironmentFromOS() *CfEnvironment {
cfe := NewCfEnvironment()

osEnvironment := SplitKeyValueStringArrayInToMap(os.Environ())
cfe.AddEnvironmentVariable(osEnvironment)

return cfe
}

func SplitKeyValueStringArrayInToMap(data []string) map[string]interface{} {
items := make(map[string]interface{})
for _, item := range data {
key, val := SplitKeyValueString(item)
items[key] = val
}
return items
}

func SplitKeyValueString(item string)(key, val string) {
splits := strings.SplitN(item, "=", 2)
key = splits[0]
val = splits[1]
return
}


func (cfe *CfEnvironment) addEnvironmentVariable(key, value string) {
cfe.env[key] = value
}

func (cfe *CfEnvironment) Environment() []string {
var commandEnvironment []string

for k, v := range cfe.env {
commandEnvironment = append(commandEnvironment, k+"="+v)
}
return commandEnvironment
}

func (cfe *CfEnvironment) AddEnvironmentVariable(switchMap map[string]interface{}) {
for k, v := range switchMap {
vString := fmt.Sprintf("%v", v)
cfe.env[k] = vString
}
}
125 changes: 125 additions & 0 deletions out/cf_environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package out_test

import (
"github.com/concourse/cf-resource/out"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"os"
)

var oneEnvironmentPair = map[string]interface{}{"ENV_ONE": "env_one"}

// json from config is unmarshalled in to map[string]interface{}
// keys are always strings, but values can be anything
var multipleEnvironmentPairs = map[string]interface{}{
"ENV_ONE": "env_one",
"ENV_TWO": 2,
"ENV_THREE": true,
}
var fiveEnvironmentPair = map[string]interface{}{"ENV_FIVE": "env_five"}

var _ = Describe("utility functions", func() {
Context("happy path", func() {

osLikeKVArray := []string{
"OS_ONE=one",
"OS_TWO=two",
"OS_THREE=three",
}

simpleKVString := "SIMPLE=pair"
keyWithValueContainingEqualsString := "EQUAL_VALUE=val_key=val_val"

It("simple k=v string splits correctly", func() {
key, value := out.SplitKeyValueString(simpleKVString)

Ω(key).Should(Equal("SIMPLE"))
Ω(value).Should(Equal("pair"))
})

It("value containing equal is parsed correctly (needs `SplitN()`)", func() {
key, value := out.SplitKeyValueString(keyWithValueContainingEqualsString)

Ω(key).Should(Equal("EQUAL_VALUE"))
Ω(value).Should(Equal("val_key=val_val"))
})

It("array of k=v strings split correctly in to map", func() {
kvMap := out.SplitKeyValueStringArrayInToMap(osLikeKVArray)

Ω(kvMap).Should(HaveLen(3))
Ω(kvMap).Should(HaveKeyWithValue("OS_ONE", "one"))
Ω(kvMap).Should(HaveKeyWithValue("OS_TWO", "two"))
Ω(kvMap).Should(HaveKeyWithValue("OS_THREE", "three"))
})
})
})

var _ = Describe("CfEnvironment from Empty", func() {
Context("happy path", func() {
var cfEnvironment *out.CfEnvironment

BeforeEach(func() {
cfEnvironment = out.NewCfEnvironment()
})

It("default command environment should ONLY contain CF_COLOR=true", func() {
cfEnv := cfEnvironment.Environment()
Ω(cfEnv).Should(HaveLen(1))
Ω(cfEnv).Should(ContainElement("CF_COLOR=true"))
})

It("added environment switch ends up in environment", func() {

cfEnvironment.AddEnvironmentVariable(oneEnvironmentPair)
cfEnv := cfEnvironment.Environment()

Ω(cfEnv).Should(HaveLen(2))
Ω(cfEnv).Should(ContainElement("ENV_ONE=env_one"))
})

It("multiple environment switches all end up in environment", func() {

cfEnvironment.AddEnvironmentVariable(multipleEnvironmentPairs)
cfEnv := cfEnvironment.Environment()

Ω(cfEnv).Should(HaveLen(4))
Ω(cfEnv).Should(ContainElement("ENV_ONE=env_one"))
Ω(cfEnv).Should(ContainElement("ENV_TWO=2"))
Ω(cfEnv).Should(ContainElement("ENV_THREE=true"))
})

It("multiple adds to environment retains all additions", func() {
cfEnvironment.AddEnvironmentVariable(multipleEnvironmentPairs)
cfEnvironment.AddEnvironmentVariable(fiveEnvironmentPair)
cfEnv := cfEnvironment.Environment()

Ω(cfEnv).Should(HaveLen(5))
Ω(cfEnv).Should(ContainElement("ENV_ONE=env_one"))
Ω(cfEnv).Should(ContainElement("ENV_TWO=2"))
Ω(cfEnv).Should(ContainElement("ENV_THREE=true"))

Ω(cfEnv).Should(ContainElement("ENV_FIVE=env_five"))
})

})
})

var _ = Describe("CfEnvironment from OS", func() {
Context("happy path", func() {
var cfEnvironment *out.CfEnvironment
env := os.Environ()
baseExpectedEnvVariables := len(env) + 1

BeforeEach(func() {
cfEnvironment = out.NewCfEnvironmentFromOS()
})

It("default command environment should contain CF_COLOR=true", func() {
cfEnv := cfEnvironment.Environment()
Ω(cfEnv).Should(HaveLen(baseExpectedEnvVariables))
Ω(cfEnv).Should(ContainElement("CF_COLOR=true"))
})
})
})
16 changes: 13 additions & 3 deletions out/cloud_foundry.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ type PAAS interface {
PushApp(manifest string, path string, currentAppName string) error
}

type CloudFoundry struct{}
type CloudFoundry struct {
cfEnvironment *CfEnvironment
}

func NewCloudFoundry() *CloudFoundry {
return &CloudFoundry{}
return &CloudFoundry{NewCfEnvironmentFromOS()}
}

func (cf *CloudFoundry) Login(api string, username string, password string, insecure bool) error {
Expand Down Expand Up @@ -74,11 +76,19 @@ func chdir(path string, f func() error) error {
return f()
}

func (cf *CloudFoundry) CommandEnvironment() *CfEnvironment {
return cf.cfEnvironment
}

func (cf *CloudFoundry) AddEnvironmentVariable(switchMap map[string]interface{}) {
cf.cfEnvironment.AddEnvironmentVariable(switchMap)
}

func (cf *CloudFoundry) cf(args ...string) *exec.Cmd {
cmd := exec.Command("cf", args...)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "CF_COLOR=true")
cmd.Env = cf.cfEnvironment.Environment()

return cmd
}
27 changes: 27 additions & 0 deletions out/cloud_foundry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package out_test

import (
"github.com/concourse/cf-resource/out"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"os"
)

var _ = Describe("CloudFoundry", func() {
Context("happy path", func() {
var cf *out.CloudFoundry
env := os.Environ()
baseExpectedEnvVariableCount := len(env) + 1

BeforeEach(func() {
cf = out.NewCloudFoundry()
})

It("default command environment should contain CF_COLOR=true", func() {
cfEnv := cf.CommandEnvironment().Environment()
Ω(cfEnv).Should(HaveLen(baseExpectedEnvVariableCount))
Ω(cfEnv).Should(ContainElement("CF_COLOR=true"))
})
})
})
2 changes: 2 additions & 0 deletions out/cmd/out/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func main() {
fatal("reading request from stdin", err)
}

cloudFoundry.AddEnvironmentVariable(request.Source.CommandEnvironmentVariables)

// make it an absolute path
request.Params.ManifestPath = filepath.Join(os.Args[1], request.Params.ManifestPath)

Expand Down
10 changes: 10 additions & 0 deletions out/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ var _ = Describe("Out", func() {
})

Context("when my manifest and paths do not contain a glob", func() {
BeforeEach(func() {
request.Source.CommandEnvironmentVariables = map[string]interface{}{
"COMMAND_ENV_ONE": "command_env_one",
"COMMAND_ENV_TWO": "command_env_two",
}
})

It("pushes an application to cloud foundry", func() {
session, err := gexec.Start(
cmd,
Expand Down Expand Up @@ -118,6 +125,9 @@ var _ = Describe("Out", func() {

// color should be always
Ω(session.Err).Should(gbytes.Say("CF_COLOR=true"))
// order is important because `env | sort` as Say fast forwards
Ω(session.Err).Should(gbytes.Say("COMMAND_ENV_ONE=command_env_one"))
Ω(session.Err).Should(gbytes.Say("COMMAND_ENV_TWO=command_env_two"))
})
})

Expand Down

0 comments on commit 0ed3201

Please sign in to comment.