Draft an implementation of inject task.
--- a/REFERENCE.md Wed Oct 11 19:40:16 2017 -0500
+++ b/REFERENCE.md Wed Oct 11 20:48:50 2017 -0500
@@ -50,6 +50,10 @@
This example shows how to expand an environment variable into a list in the context of a extended task.
+This example shows how to inject variables from a file in the build container into the plan. This example shows how you can login and logout of a Docker registry by using environment variables.
@@ -172,6 +176,38 @@
+An inject task will read a variable with environment variables and make them accessible to your plans. +A file is expected to have lines of the form `variable=value`. Empty lines are ignored. Files are read +from the docker volume, so files generated by the plan do not need to be exported in order to be a target +of an inject task. From the other direction, it is necessary to import files on the host if they are to be +| Name | Required | Default | Description | +| ---------- | -------- | ------- | ----------- | +| file | | | A file that should be read. | +| files | | | A list of files that should be read. | +| prefix | | | A prefix to add to each variable read from a file | +At least one file must be supplied by either the `file` or `files` attributes. If both are +supplied, `file` is inserted to the front of `files`. If the files being read are a result of +environment variable expansion, the order that the files are read are not guaranteed to be +stable (or in the order supplied). Be cautious of this if the environment files have define A build task will build a docker image.
--- a/docker/docker.go Wed Oct 11 19:40:16 2017 -0500
+++ b/docker/docker.go Wed Oct 11 20:48:50 2017 -0500
@@ -35,6 +35,7 @@
dockerTemplate = `docker {{if .DockerConfig}}--config {{.DockerConfig}} {{end}}`
--- a/docker/errors.go Wed Oct 11 19:40:16 2017 -0500
+++ b/docker/errors.go Wed Oct 11 20:48:50 2017 -0500
@@ -32,4 +32,5 @@
errNoImages = errors.New("no images specified")
errNoSourceTag = errors.New("no source tag specified")
errNoDestinationTags = errors.New("no destination tags specified")
+ errNoFilesInject = errors.New("no environment files specified") --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docker/inject.go Wed Oct 11 20:48:50 2017 -0500
@@ -0,0 +1,141 @@
+ * Copyright 2016-2017 Gary Kramlich <grim@reaperworld.com> + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + "github.com/aphistic/gomol" + "bitbucket.org/rw_grim/convey/environment" + "bitbucket.org/rw_grim/convey/state" + "bitbucket.org/rw_grim/convey/tasks" + "bitbucket.org/rw_grim/convey/yaml" + File string `yaml:"file"` + Files yaml.StringOrSlice `yaml:"files"` + Prefix string `yaml:"prefix"` +func (i *Inject) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error { + fullEnv := environment.Merge(env, st.GetEnv()) + prefix, err := environment.Mapper(i.Prefix, fullEnv) + files, err := st.MapSlice(i.Files, fullEnv) + // Create a temp directory we can export files from the current + // container into. We can't read directly from the volume, and + // we don't want to require reading only environment files on + tmpDir, err := ioutil.TempDir("", "convey-inject-") + defer os.RemoveAll(tmpDir) + for _, file := range files { + // Export the file into the temp directory and maintain the + // structure of the file (for ease of error messages, so we + // get the file a/b/c/env instead of env). + dest := filepath.Clean(filepath.Join(tmpDir, file)) + if err = exportFile(name, st.Workspace.Name(), file, dest, st); err != nil { + // Process the entries of the file and apply them to the + // state's base environment immediately. + entries, err := processFile(dest, file, prefix) +func processFile(path, name, prefix string) ([]string, error) { + data, err := ioutil.ReadFile(path) + return nil, fmt.Errorf("failed to read environment file '%s'", name) + for _, line := range strings.Split(string(data), "\n") { + if len(strings.TrimSpace(line)) == 0 { + // Each non-empty line requires the form key=val. Split the + // key and the val, then uppercase the key and add the prefix. + // We don't care what form val takes, it will be treated as a + // string (does not need to be quoted). + parts := strings.SplitN(line, "=", 2) + return nil, fmt.Errorf("malformed entry in environments file '%s'", line) + key = strings.TrimSpace(parts[0]) + val = strings.TrimSpace(parts[1]) + return nil, fmt.Errorf("malformed entry in environments file '%s'", line) + entries = append(entries, fmt.Sprintf("%s%s=%s", prefix, strings.ToUpper(key), val)) +func (i *Inject) New() tasks.Task { +func (i *Inject) Valid() error { + i.Files = append([]string{i.File}, i.Files...) + return errNoFilesInject --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/docker/inject_test.go Wed Oct 11 20:48:50 2017 -0500
@@ -0,0 +1,58 @@
+ * Copyright 2016-2017 Gary Kramlich <grim@reaperworld.com> + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + "github.com/aphistic/sweet" + "github.com/go-yaml/yaml" + . "github.com/onsi/gomega" + cYaml "bitbucket.org/rw_grim/convey/yaml" +func (s *dockerSuite) TestInject(t sweet.T) { + i := &Inject{Files: cYaml.StringOrSlice{"foo"}} + Expect(i.Valid()).To(BeNil()) +func (s *dockerSuite) TestInjectFilesRequired(t sweet.T) { + Expect(i.Valid()).To(MatchError(errNoFilesInject)) +func (s *dockerSuite) TestInjectUnmarshalString(t sweet.T) { + data := `files: filename` + err := yaml.Unmarshal([]byte(data), &imp) + Expect(err).To(BeNil()) + Expect(imp.Files).To(Equal(cYaml.StringOrSlice{"filename"})) +func (s *dockerSuite) TestInjectUnmarshalStringSlice(t sweet.T) { + err := yaml.Unmarshal([]byte(data), &imp) + Expect(err).To(BeNil()) + Expect(imp.Files).To(Equal(cYaml.StringOrSlice{"filename1", "filename2"})) --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/inject.yml Wed Oct 11 20:48:50 2017 -0500
@@ -0,0 +1,56 @@
+# This plan shows how the inject plan can add additional +# environment variables accessible to your tasks during +# runtime. Running this with `-e X_FOO=4`, the output +# will be (in two parts): +# Notice that before the inject task, only X_FOO and X_BAR +# were set by the command line argument and the global env +# block, respectively. Afterwards, X_FOO's value did not +# change (as command line parameters take have precedence), +# X_BAR is overwritten, and X_BAZ is newly added. + image: gliderlabs/alpine:edge + image: gliderlabs/alpine:edge + - echo "X_FOO=${X_FOO}" + - echo "X_BAR=${X_BAR}" + - echo "X_BAZ=${X_BAZ}" --- a/intrinsic/intrinsic.go Wed Oct 11 19:40:16 2017 -0500
+++ b/intrinsic/intrinsic.go Wed Oct 11 20:48:50 2017 -0500
@@ -25,6 +25,5 @@
Tasks = map[string]tasks.Task{
--- a/state/state.go Wed Oct 11 19:40:16 2017 -0500
+++ b/state/state.go Wed Oct 11 20:48:50 2017 -0500
@@ -119,14 +119,16 @@
-// MergeEnv adds additional environment arguments to the original state environment.
+// MergeEnv adds additional environment arguments with lowere precedence to +// the original state environment. If they duplicate an existing environment +// variable, that value will be unchanged. func (st *State) MergeEnv(env []string) {
- st.innerEnv = environment.Merge(st.innerEnv, env)
+ st.innerEnv = environment.Merge(env, st.innerEnv) // MapSlice calls SliceMapper on the given environment, but also checks to