Skip to content

Commit 6b7fc0e

Browse files
committed
feat: remote state files
This change enhances helmfile to accept terraform-module-like URLs in nested state files a.k.a sub-helmfiles. ```yaml helmfiles: - # Terraform-module-like URL for importing a remote directory and use a file in it as a nested-state file # The nested-state file is locally checked-out along with the remote directory containing it. # Therefore all the local paths in the file are resolved relative to the file path: git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0 ``` The URL isn't equivalent to terraform module sources. The difference is that we use `@` to distinguish between (1) the path to the repository and directory containing the state file and (2) the path to the state file being loaded. This distinction provides us enough fleibiity to instruct helmfile to check-out necessary and sufficient directory to make the state file works. Under the hood, it uses [hashicorp/go-getter](https://github.com/hashicorp/go-getter), that is used for [terraform module sources](https://www.terraform.io/docs/modules/sources.html) as well. Only the git provider without authentication like git-credentials helper is tested. But theoretically any go-getter providers should work. Please feel free to test the provider of your choice and contribute documentation or instruction to use it :) Resolves #347
1 parent 3710f62 commit 6b7fc0e

12 files changed

+579
-28
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ helmfiles:
184184
- # All the nested state files under `helmfiles:` is processed in the order of definition.
185185
# So it can be used for preparation for your main `releases`. An example would be creating CRDs required by `reelases` in the parent state file.
186186
path: path/to/mycrd.helmfile.yaml
187+
- # Terraform-module-like URL for importing a remote directory and use a file in it as a nested-state file
188+
# The nested-state file is locally checked-out along with the remote directory containing it.
189+
# Therefore all the local paths in the file are resolved relative to the file
190+
path: git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0
187191

188192
#
189193
# Advanced Configuration: Environments

go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,15 @@ require (
88
github.com/aokoli/goutils v1.0.1 // indirect
99
github.com/google/go-cmp v0.3.0
1010
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect
11+
github.com/hashicorp/go-getter v1.3.0
1112
github.com/huandu/xstrings v1.0.0 // indirect
1213
github.com/imdario/mergo v0.3.6
13-
github.com/mattn/go-runewidth v0.0.4 // indirect
1414
github.com/pkg/errors v0.8.1 // indirect
1515
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
1616
github.com/urfave/cli v0.0.0-20160620154522-6011f165dc28
1717
go.uber.org/atomic v1.3.2 // indirect
1818
go.uber.org/multierr v1.1.0 // indirect
1919
go.uber.org/zap v1.8.0
20-
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc // indirect
2120
gopkg.in/yaml.v2 v2.2.1
2221
gotest.tools v2.2.0+incompatible
2322
)

go.sum

+167
Large diffs are not rendered by default.

pkg/app/app.go

+32
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package app
33
import (
44
"fmt"
55
"github.com/roboll/helmfile/pkg/helmexec"
6+
"github.com/roboll/helmfile/pkg/remote"
67
"github.com/roboll/helmfile/pkg/state"
78
"io/ioutil"
89
"log"
@@ -42,6 +43,8 @@ type App struct {
4243

4344
getwd func() (string, error)
4445
chdir func(string) error
46+
47+
remote *remote.Remote
4548
}
4649

4750
func New(conf ConfigProvider) *App {
@@ -302,6 +305,14 @@ func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*sta
302305
} else {
303306
optsForNestedState.Selectors = m.Selectors
304307
}
308+
309+
path, err := a.remote.Locate(m.Path)
310+
if err != nil {
311+
return appError(fmt.Sprintf("in .helmfiles[%d]", i), err)
312+
}
313+
314+
m.Path = path
315+
305316
if err := a.visitStates(m.Path, optsForNestedState, converge); err != nil {
306317
switch err.(type) {
307318
case *NoMatchingHelmfileError:
@@ -373,6 +384,26 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
373384
opts.Environment.OverrideValues = envvals
374385
}
375386

387+
var dir string
388+
if a.directoryExistsAt(fileOrDir) {
389+
dir = fileOrDir
390+
} else {
391+
dir = filepath.Dir(fileOrDir)
392+
}
393+
394+
getter := &remote.GoGetter{Logger: a.Logger}
395+
396+
remote := &remote.Remote{
397+
Logger: a.Logger,
398+
Home: dir,
399+
Getter: getter,
400+
ReadFile: a.readFile,
401+
DirExists: a.directoryExistsAt,
402+
FileExists: a.fileExistsAt,
403+
}
404+
405+
a.remote = remote
406+
376407
err := a.visitStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
377408
if len(st.Selectors) > 0 {
378409
err := st.FilterReleases()
@@ -388,6 +419,7 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge
388419
type Key struct {
389420
TillerNamespace, Name string
390421
}
422+
391423
releaseNameCounts := map[Key]int{}
392424
for _, r := range st.Releases {
393425
tillerNamespace := st.HelmDefaults.TillerNamespace

pkg/app/app_test.go

+17-16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"github.com/roboll/helmfile/pkg/helmexec"
66
"github.com/roboll/helmfile/pkg/state"
7+
"github.com/roboll/helmfile/pkg/testhelper"
78
"os"
89
"path/filepath"
910
"reflect"
@@ -13,11 +14,11 @@ import (
1314
)
1415

1516
func appWithFs(app *App, files map[string]string) *App {
16-
fs := state.NewTestFs(files)
17+
fs := testhelper.NewTestFs(files)
1718
return injectFs(app, fs)
1819
}
1920

20-
func injectFs(app *App, fs *state.TestFs) *App {
21+
func injectFs(app *App, fs *testhelper.TestFs) *App {
2122
app.readFile = fs.ReadFile
2223
app.glob = fs.Glob
2324
app.abs = fs.Abs
@@ -52,7 +53,7 @@ releases:
5253
chart: stable/grafana
5354
`,
5455
}
55-
fs := state.NewTestFs(files)
56+
fs := testhelper.NewTestFs(files)
5657
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
5758
app := &App{
5859
KubeContext: "default",
@@ -98,7 +99,7 @@ BAR: 2
9899
BAZ: 4
99100
`,
100101
}
101-
fs := state.NewTestFs(files)
102+
fs := testhelper.NewTestFs(files)
102103
fs.GlobFixtures["/path/to/env.*.yaml"] = []string{"/path/to/env.2.yaml", "/path/to/env.1.yaml"}
103104
app := &App{
104105
KubeContext: "default",
@@ -137,7 +138,7 @@ releases:
137138
chart: stable/zipkin
138139
`,
139140
}
140-
fs := state.NewTestFs(files)
141+
fs := testhelper.NewTestFs(files)
141142
app := &App{
142143
KubeContext: "default",
143144
Logger: helmexec.NewLogger(os.Stderr, "debug"),
@@ -190,7 +191,7 @@ releases:
190191
chart: stable/zipkin
191192
`, testcase.handler, testcase.filePattern),
192193
}
193-
fs := state.NewTestFs(files)
194+
fs := testhelper.NewTestFs(files)
194195
app := &App{
195196
KubeContext: "default",
196197
Logger: helmexec.NewLogger(os.Stderr, "debug"),
@@ -251,7 +252,7 @@ releases:
251252
}
252253

253254
for _, testcase := range testcases {
254-
fs := state.NewTestFs(files)
255+
fs := testhelper.NewTestFs(files)
255256
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
256257
app := &App{
257258
KubeContext: "default",
@@ -1077,7 +1078,7 @@ releases:
10771078
stage: post
10781079
<<: *default
10791080
`
1080-
testFs := state.NewTestFs(map[string]string{
1081+
testFs := testhelper.NewTestFs(map[string]string{
10811082
yamlFile: yamlContent,
10821083
"/path/to/base.yaml": `environments:
10831084
default:
@@ -1158,7 +1159,7 @@ releases:
11581159
stage: post
11591160
<<: *default
11601161
`
1161-
testFs := state.NewTestFs(map[string]string{
1162+
testFs := testhelper.NewTestFs(map[string]string{
11621163
yamlFile: yamlContent,
11631164
"/path/to/base.yaml": `environments:
11641165
default:
@@ -1235,7 +1236,7 @@ releases:
12351236
- name: myrelease0
12361237
chart: mychart0
12371238
`
1238-
testFs := state.NewTestFs(map[string]string{
1239+
testFs := testhelper.NewTestFs(map[string]string{
12391240
yamlFile: yamlContent,
12401241
"/path/to/base.yaml": `environments:
12411242
default:
@@ -1295,7 +1296,7 @@ releases:
12951296
- name: myrelease0
12961297
chart: mychart0
12971298
`
1298-
testFs := state.NewTestFs(map[string]string{
1299+
testFs := testhelper.NewTestFs(map[string]string{
12991300
yamlFile: yamlContent,
13001301
"/path/to/base.yaml": `environments:
13011302
default:
@@ -1372,7 +1373,7 @@ releases:
13721373
stage: post
13731374
<<: *default
13741375
`
1375-
testFs := state.NewTestFs(map[string]string{
1376+
testFs := testhelper.NewTestFs(map[string]string{
13761377
yamlFile: yamlContent,
13771378
"/path/to/base.yaml": `environments:
13781379
test:
@@ -1458,7 +1459,7 @@ releases:
14581459
chart: mychart3
14591460
<<: *default
14601461
`
1461-
testFs := state.NewTestFs(map[string]string{
1462+
testFs := testhelper.NewTestFs(map[string]string{
14621463
yamlFile: yamlContent,
14631464
"/path/to/yaml/templates.yaml": `templates:
14641465
default: &default
@@ -1515,7 +1516,7 @@ releases:
15151516
- name: {{ .Environment.Values.foo | quote }}
15161517
chart: {{ .Environment.Values.bar | quote }}
15171518
`
1518-
testFs := state.NewTestFs(map[string]string{
1519+
testFs := testhelper.NewTestFs(map[string]string{
15191520
statePath: stateContent,
15201521
"/path/to/1.yaml": `bar: ["bar"]`,
15211522
"/path/to/2.yaml": `bar: ["BAR"]`,
@@ -1568,7 +1569,7 @@ releases:
15681569
- name: {{ .Environment.Values.foo | quote }}
15691570
chart: {{ .Environment.Values.bar | quote }}
15701571
`
1571-
testFs := state.NewTestFs(map[string]string{
1572+
testFs := testhelper.NewTestFs(map[string]string{
15721573
statePath: stateContent,
15731574
"/path/to/1.yaml": `bar: ["bar"]`,
15741575
"/path/to/2.yaml": `bar: ["BAR"]`,
@@ -1653,7 +1654,7 @@ releases:
16531654
tc := testcases[i]
16541655
statePath := "/path/to/helmfile.yaml"
16551656
stateContent := fmt.Sprintf(tc.state, tc.expr)
1656-
testFs := state.NewTestFs(map[string]string{
1657+
testFs := testhelper.NewTestFs(map[string]string{
16571658
statePath: stateContent,
16581659
"/path/to/1.yaml": `foo: FOO`,
16591660
"/path/to/2.yaml": `bar: { "baz": "BAZ" }

pkg/app/two_pass_renderer_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ package app
33
import (
44
"github.com/roboll/helmfile/pkg/helmexec"
55
"github.com/roboll/helmfile/pkg/state"
6+
"github.com/roboll/helmfile/pkg/testhelper"
67
"os"
78
"strings"
89
"testing"
910

1011
"gopkg.in/yaml.v2"
1112
)
1213

13-
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *state.TestFs) {
14-
testfs := state.NewTestFs(files)
14+
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *testhelper.TestFs) {
15+
testfs := testhelper.NewTestFs(files)
1516
return &desiredStateLoader{
1617
env: env,
1718
namespace: "namespace",

0 commit comments

Comments
 (0)