Skip to content

Commit 2a7884e

Browse files
committed
interpret certain values in provisioning files as templates
This provides a convenient way of creating provisioning files that can be controlled by parameters.
1 parent 3e8945a commit 2a7884e

File tree

7 files changed

+279
-46
lines changed

7 files changed

+279
-46
lines changed

cmd/image_build.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
func imageBuildCommand() *cobra.Command {
1414
var vmID uint
1515
var provisionFile string
16-
var provisionValues []string
16+
var provisionOverrides []string
1717

1818
var vcpus uint
1919

@@ -74,8 +74,8 @@ step, and then committing the resulting volume.`,
7474
}
7575

7676
provOpt := virter.ProvisionOption{
77-
FilePath: provisionFile,
78-
Values: provisionValues,
77+
FilePath: provisionFile,
78+
Overrides: provisionOverrides,
7979
}
8080

8181
provisionConfig, err := virter.NewProvisionConfig(provOpt)
@@ -113,7 +113,7 @@ step, and then committing the resulting volume.`,
113113
}
114114

115115
buildCmd.Flags().StringVarP(&provisionFile, "provision", "p", "", "name of toml file containing provisioning steps")
116-
buildCmd.Flags().StringSliceVarP(&provisionValues, "set", "s", []string{}, "set/override provisioning steps")
116+
buildCmd.Flags().StringSliceVarP(&provisionOverrides, "set", "s", []string{}, "set/override provisioning steps")
117117
buildCmd.Flags().UintVarP(&vmID, "id", "", 0, "ID for VM which determines the IP address")
118118
buildCmd.Flags().UintVar(&vcpus, "vcpus", 1, "Number of virtual CPUs to allocate for the VM")
119119

cmd/vm_exec.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ import (
44
"context"
55
"strings"
66

7+
log "github.com/sirupsen/logrus"
8+
79
"github.com/LINBIT/virter/internal/virter"
810
"github.com/LINBIT/virter/pkg/netcopy"
9-
log "github.com/sirupsen/logrus"
1011

1112
"github.com/spf13/cobra"
1213
)
1314

1415
func vmExecCommand() *cobra.Command {
1516
var provisionFile string
16-
var provisionValues []string
17+
var provisionOverrides []string
1718

1819
execCmd := &cobra.Command{
1920
Use: "exec vm_name [vm_name...]",
@@ -22,8 +23,8 @@ func vmExecCommand() *cobra.Command {
2223
Args: cobra.MinimumNArgs(1),
2324
Run: func(cmd *cobra.Command, args []string) {
2425
provOpt := virter.ProvisionOption{
25-
FilePath: provisionFile,
26-
Values: provisionValues,
26+
FilePath: provisionFile,
27+
Overrides: provisionOverrides,
2728
}
2829
if err := execProvision(provOpt, args); err != nil {
2930
log.Fatal(err)
@@ -32,7 +33,7 @@ func vmExecCommand() *cobra.Command {
3233
}
3334

3435
execCmd.Flags().StringVarP(&provisionFile, "provision", "p", "", "name of toml file containing provisioning steps")
35-
execCmd.Flags().StringSliceVarP(&provisionValues, "set", "s", []string{}, "set/override provisioning steps")
36+
execCmd.Flags().StringSliceVarP(&provisionOverrides, "set", "s", []string{}, "set/override provisioning steps")
3637

3738
return execCmd
3839
}

doc/provisioning.md

+26-13
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,38 @@ It can also be applied to one or multiple already running VMs:
1616
$ virter vm exec -p provisioning.toml centos-1 centos-2 centos-3
1717
```
1818

19+
## Provisioning types
20+
1921
The following provisioning types are supported.
2022

21-
## `docker`
23+
### `docker`
2224
A `docker` provisioning step allows specifying a Docker image to run provisioning steps in. This image will be executed on the host, so it will need to connect to the target VM and run its provisioning commands over SSH (or use a provisioning tool such as Ansible).
2325

24-
### Configuration Options
2526
The Docker provisioning step can be parameterized using the following configuration options:
26-
* `image` is the Docker image used to provision the VM. It follows the standard Docker format of `<repository>/<image>:<tag>`.
27-
* `env` is a map of environment variables to be passed to the Docker container, in `KEY=value` format.
27+
* `image` is the Docker image used to provision the VM. It follows the standard Docker format of `<repository>/<image>:<tag>`. This is a Go template.
28+
* `env` is a map of environment variables to be passed to the Docker container, in `KEY=value` format. The values are Go templates.
2829

2930
Note that Virter already passes two environment variables by default:
3031
* `TARGETS` is a comma separated list of all VMs to run the provisioning on.
3132
* `SSH_PRIVATE_KEY` is the SSH private key Virter uses to connect to the machine as `root`.
3233

33-
## Shell
34+
### Shell
3435
The `shell` provisioning step allows running arbitrary commands on the target VM over SSH. This is easier to use than the `docker` step, but also less flexible.
3536

36-
### Configuration Options
3737
The `shell` provisioning step accepts the following parameters:
3838
* `script` is a string containing the command(s) to be run.
3939
It can be either a single line string to run only a single command, or a multi-line string (as defined by toml), in which case every line of the string will be considered a separate command to run.
40-
* `env` is a map of environment variables to be set in the target VM, in `KEY=value` format.
40+
* `env` is a map of environment variables to be set in the target VM, in `KEY=value` format. The values are Go templates.
4141

42-
## rsync
42+
### rsync
4343

4444
The `rsync` provisioning step can be used to distribute files from the host to the guest machines using the `rsync` utility.
4545

4646
**NOTE**: This step requires that the `rsync` program is installed both on the host and on the guest machine. The user is responsible for making sure that this requirement is met.
4747

48-
### Configuration Options
49-
48+
The `rsync` provisioning step accepts the following parameters:
5049
* `source` is as a glob pattern of files on the host machine.
51-
It will first be expanded by Go's [os.ExpandEnv](https://golang.org/pkg/os/#ExpandEnv) and
50+
It will first be expanded as a Go template and
5251
then interpreted according to the rules of Go's [filepath.Match](https://golang.org/pkg/path/filepath/#Match)
5352
function, so refer to the Go documentation for details.
5453
* `dest` is the path on the guest machine(s) where the files should be copied to.
@@ -59,16 +58,30 @@ The glob-expanded `source` list of files and the `dest` path are passed verbatim
5958

6059
There are also global options which can be set for all provisioning steps in a file.
6160

62-
* `env` is a map of environment variables in `KEY=value` format. These will be set in all provisioning steps that support `env` by themselves.
61+
* `env` is a map of environment variables in `KEY=value` format. These will be set in all provisioning steps that support `env` by themselves. The values are Go templates.
62+
63+
## Template values
64+
65+
As documented for the various provisioning types above, many of the values in a provisioning file are interpreted as
66+
[Go templates](https://golang.org/pkg/text/template/).
67+
68+
The data provided to these templates is the `[values]` section in the provisioning file. These values can be set or overridden with `--set`. For instance:
69+
```
70+
$ virter vm exec my-vm -p examples/hello-world/hello-world.toml --set values.Image=my-image-name
71+
```
6372

6473
## Example
6574
```
75+
[values]
76+
Image = "virter-hello-text"
77+
6678
[env]
6779
foo = "rck"
6880
6981
[[steps]]
7082
[steps.docker]
71-
image = "virter-hello-text:latest"
83+
# Go templating can be used for many values
84+
image = "{{.Image}}"
7285
[steps.docker.env]
7386
TEXT = "foo"
7487
VAR_BAR = "hi"

examples/hello-world/hello-world.toml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[values]
2+
Image = "virter-hello-world"
3+
4+
[[steps]]
5+
[steps.docker]
6+
image = "{{.Image}}"

internal/virter/provision.go

+52-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package virter
22

33
import (
4+
"bytes"
5+
"fmt"
46
"io"
57
"os"
8+
"text/template"
69

710
"github.com/BurntSushi/toml"
811
"github.com/helm/helm/pkg/strvals"
@@ -36,8 +39,9 @@ type ProvisionStep struct {
3639

3740
// ProvisionConfig holds the configuration of the whole provisioning
3841
type ProvisionConfig struct {
39-
Env map[string]string `toml:"env"`
40-
Steps []ProvisionStep `toml:"steps"`
42+
Values map[string]string `toml:"values"`
43+
Env map[string]string `toml:"env"`
44+
Steps []ProvisionStep `toml:"steps"`
4145
}
4246

4347
// NeedsDocker checks if there is a provision step that requires a docker client
@@ -77,8 +81,8 @@ func EnvmapToSlice(envMap map[string]string) []string {
7781

7882
// ProvisionOption sumarizes all the options used for generating the final ProvisionConfig
7983
type ProvisionOption struct {
80-
FilePath string
81-
Values []string
84+
FilePath string
85+
Overrides []string
8286
}
8387

8488
// NewProvisionConfig returns a ProvisionConfig from a ProvisionOption
@@ -116,11 +120,27 @@ func newProvisionConfigReader(provReader io.Reader, provOpt ProvisionOption) (Pr
116120
return pc, err
117121
}
118122

119-
for _, s := range pc.Steps {
123+
for i, s := range pc.Steps {
120124
if s.Docker != nil {
121125
s.Docker.Env = mergeEnv(&pc.Env, &s.Docker.Env)
126+
127+
if s.Docker.Image, err = executeTemplate(s.Docker.Image, pc.Values); err != nil {
128+
return pc, fmt.Errorf("failed to execute template for docker.image for step %d: %w", i, err)
129+
}
130+
131+
if err := executeTemplates(s.Docker.Env, pc.Values); err != nil {
132+
return pc, fmt.Errorf("failed to execute template for docker.env for step %d: %w", i, err)
133+
}
122134
} else if s.Shell != nil {
123135
s.Shell.Env = mergeEnv(&pc.Env, &s.Shell.Env)
136+
137+
if err := executeTemplates(s.Shell.Env, pc.Values); err != nil {
138+
return pc, fmt.Errorf("failed to execute template for shell.env for step %d: %w", i, err)
139+
}
140+
} else if s.Rsync != nil {
141+
if s.Rsync.Source, err = executeTemplate(s.Rsync.Source, pc.Values); err != nil {
142+
return pc, fmt.Errorf("failed to execute template for rsync.source for step %d: %w", i, err)
143+
}
124144
}
125145
}
126146

@@ -130,11 +150,37 @@ func newProvisionConfigReader(provReader io.Reader, provOpt ProvisionOption) (Pr
130150
func genValueMap(provOpt ProvisionOption) (map[string]interface{}, error) {
131151
base := map[string]interface{}{}
132152

133-
for _, value := range provOpt.Values {
153+
for _, value := range provOpt.Overrides {
134154
if err := strvals.ParseInto(value, base); err != nil {
135155
return base, err
136156
}
137157
}
138158

139159
return base, nil
140160
}
161+
162+
func executeTemplates(templates map[string]string, templateData map[string]string) error {
163+
for k, v := range templates {
164+
result, err := executeTemplate(v, templateData)
165+
if err != nil {
166+
return err
167+
}
168+
templates[k] = result
169+
}
170+
return nil
171+
}
172+
173+
func executeTemplate(templateText string, templateData map[string]string) (string, error) {
174+
tmpl, err := template.New("").Option("missingkey=error").Parse(templateText)
175+
if err != nil {
176+
return "", err
177+
}
178+
179+
var b bytes.Buffer
180+
err = tmpl.Execute(&b, templateData)
181+
if err != nil {
182+
return "", err
183+
}
184+
185+
return b.String(), nil
186+
}

0 commit comments

Comments
 (0)