Skip to content

Commit e00382b

Browse files
authored
Merge pull request #36 from jumppad-labs/f-disabled-interpolation
Enable disabled to be set by an interpolated value or from a module value.
2 parents d9abe8b + fbd25c7 commit e00382b

File tree

9 files changed

+236
-130
lines changed

9 files changed

+236
-130
lines changed

config.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func (c *Config) findResource(path string) (types.Resource, error) {
102102
}
103103
}
104104

105-
return nil, ResourceNotFoundError{path}
105+
return nil, ResourceNotFoundError{fqdn.StringWithoutAttribute()}
106106
}
107107

108108
func (c *Config) FindRelativeResource(path string, parentModule string) (types.Resource, error) {

dag.go

+128-88
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,6 @@ func doYaLikeDAGs(c *Config) (*dag.AcyclicGraph, error) {
4343
continue
4444
}
4545

46-
// we might not yet know if the resource is disabled, this could be due
47-
// to the value being set from a variable or an interpolated value
48-
49-
// if disabled ignore any dependencies
50-
if resource.GetDisabled() {
51-
// add all disabled resources to the root
52-
//fmt.Println("connect", "root", "to", resource.Metadata().ID)
53-
54-
graph.Connect(dag.BasicEdge(root, resource))
55-
continue
56-
}
57-
5846
// use a map to keep a unique list
5947
dependencies := map[types.Resource]bool{}
6048

@@ -87,17 +75,10 @@ func doYaLikeDAGs(c *Config) (*dag.AcyclicGraph, error) {
8775
// then the reference should be modified to include the parent reference
8876
// "module.module1.module2.resource.container.mine.id"
8977
relFQDN := fqdn.AppendParentModule(resource.Metadata().Module)
90-
deps, err := c.FindModuleResources(relFQDN.String(), true)
91-
if err != nil {
92-
pe := &errors.ParserError{}
93-
pe.Line = resource.Metadata().Line
94-
pe.Column = resource.Metadata().Column
95-
pe.Filename = resource.Metadata().File
96-
pe.Message = fmt.Sprintf("unable to find resources for module: %s, error: %s", fqdn.Module, err)
97-
pe.Level = errors.ParserErrorLevelError
9878

99-
return nil, pe
100-
}
79+
// we ignore the error here as it may be possible that the module depends on
80+
// disabled resources
81+
deps, _ := c.FindModuleResources(relFQDN.String(), true)
10182

10283
for _, dep := range deps {
10384
dependencies[dep] = true
@@ -112,17 +93,10 @@ func doYaLikeDAGs(c *Config) (*dag.AcyclicGraph, error) {
11293
// then the reference should be modified to include the parent reference
11394
// "module.module1.module2.resource.container.mine.id"
11495
relFQDN := fqdn.AppendParentModule(resource.Metadata().Module)
115-
dep, err := c.FindResource(relFQDN.String())
116-
if err != nil {
117-
pe := &errors.ParserError{}
118-
pe.Line = resource.Metadata().Line
119-
pe.Column = resource.Metadata().Column
120-
pe.Filename = resource.Metadata().File
121-
pe.Message = fmt.Sprintf("unable to find dependent resource in module: '%s', error: '%s'", resource.Metadata().Module, err)
122-
pe.Level = errors.ParserErrorLevelError
12396

124-
return nil, pe
125-
}
97+
// we ignore the error here as it may be possible that the module depends on
98+
// disabled resources
99+
dep, _ := c.FindResource(relFQDN.String())
126100

127101
dependencies[dep] = true
128102
}
@@ -138,7 +112,7 @@ func doYaLikeDAGs(c *Config) (*dag.AcyclicGraph, error) {
138112
pe.Line = resource.Metadata().Line
139113
pe.Column = resource.Metadata().Column
140114
pe.Filename = resource.Metadata().File
141-
pe.Message = fmt.Sprintf("unable to find resources parent module: '%s, error: %s", fqdnString, err)
115+
pe.Message = fmt.Sprintf("unable to find parent module: '%s', error: %s", fqdnString, err)
142116
pe.Level = errors.ParserErrorLevelError
143117

144118
return nil, pe
@@ -178,7 +152,7 @@ func createCallback(c *Config, wf WalkCallback) func(v dag.Vertex) (diags dag.Di
178152
}
179153

180154
// if this is the root module or is disabled skip or is a variable
181-
if (r.Metadata().Type == resources.TypeRoot) || r.GetDisabled() {
155+
if r.Metadata().Type == resources.TypeRoot {
182156
return nil
183157
}
184158

@@ -192,77 +166,62 @@ func createCallback(c *Config, wf WalkCallback) func(v dag.Vertex) (diags dag.Di
192166
panic("no context found for resource")
193167
}
194168

195-
// attempt to set the values in the resource links to the resource attribute
196-
// all linked values should now have been processed as the graph
197-
// will have handled them first
198-
for _, v := range r.Metadata().Links {
199-
fqrn, err := resources.ParseFQRN(v)
200-
if err != nil {
201-
pe := &errors.ParserError{}
202-
pe.Filename = r.Metadata().File
203-
pe.Line = r.Metadata().Line
204-
pe.Column = r.Metadata().Column
205-
pe.Message = fmt.Sprintf("error parsing resource link %s", err)
206-
pe.Level = errors.ParserErrorLevelError
207-
208-
return diags.Append(pe)
209-
}
169+
// first we need to check if the resource is disabled
170+
// this might be set by an interpolated value
171+
// if this is disabled we ignore the resource
172+
//
173+
// This expression could be a reference to another resource or it could be a
174+
// function or a conditional statement. We need to evaluate the expression
175+
// to determine if the resource should be disabled
176+
if attr, ok := bdy.Attributes["disabled"]; ok {
177+
expr, err := processExpr(attr.Expr)
210178

211-
// get the value from the linked resource
212-
l, err := c.FindRelativeResource(v, r.Metadata().Module)
179+
// need to handle this error
213180
if err != nil {
214181
pe := &errors.ParserError{}
215182
pe.Filename = r.Metadata().File
216183
pe.Line = r.Metadata().Line
217184
pe.Column = r.Metadata().Column
218-
pe.Message = fmt.Sprintf(`unable to find dependent resource "%s" %s`, v, err)
185+
pe.Message = fmt.Sprintf(`unable to process disabled expression: %s`, err)
219186
pe.Level = errors.ParserErrorLevelError
220187

221188
return diags.Append(pe)
222189
}
223190

224-
var ctyRes cty.Value
225-
226-
// once we have found a resource convert it to a cty type and then
227-
// set it on the context
228-
switch l.Metadata().Type {
229-
case resources.TypeLocal:
230-
loc := l.(*resources.Local)
231-
ctyRes = loc.CtyValue
232-
case resources.TypeOutput:
233-
out := l.(*resources.Output)
234-
ctyRes = out.CtyValue
235-
default:
236-
ctyRes, err = convert.GoToCtyValue(l)
237-
}
238-
239-
if err != nil {
240-
pe := &errors.ParserError{}
241-
pe.Filename = r.Metadata().File
242-
pe.Line = r.Metadata().Line
243-
pe.Column = r.Metadata().Column
244-
pe.Message = fmt.Sprintf(`unable to convert reference %s to context variable: %s`, v, err)
245-
pe.Level = errors.ParserErrorLevelError
191+
if len(expr) > 0 {
192+
// first we need to build the context for the expression
193+
err := setContextVariablesFromList(c, r, expr, ctx)
194+
if err != nil {
195+
return diags.Append(err)
196+
}
246197

247-
return diags.Append(pe)
248-
}
198+
// now we need to evaluate the expression
199+
var isDisabled bool
200+
expdiags := gohcl.DecodeExpression(attr.Expr, ctx, &isDisabled)
201+
if expdiags.HasErrors() {
249202

250-
// remove the attributes and to get a pure resource ref
251-
fqrn.Attribute = ""
203+
pe := &errors.ParserError{}
204+
pe.Filename = r.Metadata().File
205+
pe.Line = r.Metadata().Line
206+
pe.Column = r.Metadata().Column
207+
pe.Message = fmt.Sprintf(`unable to process disabled expression: %s`, expdiags.Error())
208+
pe.Level = errors.ParserErrorLevelError
252209

253-
err = setContextVariableFromPath(ctx, fqrn.String(), ctyRes)
254-
if err != nil {
255-
pe := &errors.ParserError{}
256-
pe.Filename = r.Metadata().File
257-
pe.Line = r.Metadata().Line
258-
pe.Column = r.Metadata().Column
259-
pe.Message = fmt.Sprintf(`unable to set context variable: %s`, err)
260-
pe.Level = errors.ParserErrorLevelError
210+
return diags.Append(pe)
211+
}
261212

262-
return diags.Append(pe)
213+
r.SetDisabled(isDisabled)
263214
}
264215
}
265216

217+
// if the resource is disabled we need to skip the resource
218+
if r.GetDisabled() {
219+
return nil
220+
}
221+
222+
// set the context variables from the linked resources
223+
setContextVariablesFromList(c, r, r.Metadata().Links, ctx)
224+
266225
// Process the raw resource now we have the context from the linked
267226
// resources
268227
ul := getContextLock(ctx)
@@ -301,8 +260,9 @@ func createCallback(c *Config, wf WalkCallback) func(v dag.Vertex) (diags dag.Di
301260
return diags.Append(pe)
302261
}
303262

304-
// if the type is a module the potentially we only just found out that we should be
263+
// if the type is a module then potentially we only just found out that we should be
305264
// disabled
265+
306266
// as an additional check, set all module resources to disabled if the module is disabled
307267
if r.GetDisabled() && r.Metadata().Type == resources.TypeModule {
308268
// find all dependent resources
@@ -384,3 +344,83 @@ func createCallback(c *Config, wf WalkCallback) func(v dag.Vertex) (diags dag.Di
384344
return nil
385345
}
386346
}
347+
348+
// setContextVariablesFromList sets the context variables from a list of resource links
349+
//
350+
// for example: given the values ["module.module1.module2.resource.container.mine.id"]
351+
// the context variable "module.module1.module2.resource.container.mine.id" will be set to the
352+
// value defined by the resource of type container with the name mine and the attribute id
353+
func setContextVariablesFromList(c *Config, r types.Resource, values []string, ctx *hcl.EvalContext) *errors.ParserError {
354+
// attempt to set the values in the resource links to the resource attribute
355+
// all linked values should now have been processed as the graph
356+
// will have handled them first
357+
for _, v := range values {
358+
fqrn, err := resources.ParseFQRN(v)
359+
if err != nil {
360+
pe := &errors.ParserError{}
361+
pe.Filename = r.Metadata().File
362+
pe.Line = r.Metadata().Line
363+
pe.Column = r.Metadata().Column
364+
pe.Message = fmt.Sprintf("error parsing resource link %s", err)
365+
pe.Level = errors.ParserErrorLevelError
366+
367+
return pe
368+
}
369+
370+
// get the value from the linked resource
371+
l, err := c.FindRelativeResource(v, r.Metadata().Module)
372+
if err != nil {
373+
pe := &errors.ParserError{}
374+
pe.Filename = r.Metadata().File
375+
pe.Line = r.Metadata().Line
376+
pe.Column = r.Metadata().Column
377+
pe.Message = fmt.Sprintf(`unable to find dependent resource "%s" %s`, v, err)
378+
pe.Level = errors.ParserErrorLevelError
379+
380+
return pe
381+
}
382+
383+
var ctyRes cty.Value
384+
385+
// once we have found a resource convert it to a cty type and then
386+
// set it on the context
387+
switch l.Metadata().Type {
388+
case resources.TypeLocal:
389+
loc := l.(*resources.Local)
390+
ctyRes = loc.CtyValue
391+
case resources.TypeOutput:
392+
out := l.(*resources.Output)
393+
ctyRes = out.CtyValue
394+
default:
395+
ctyRes, err = convert.GoToCtyValue(l)
396+
}
397+
398+
if err != nil {
399+
pe := &errors.ParserError{}
400+
pe.Filename = r.Metadata().File
401+
pe.Line = r.Metadata().Line
402+
pe.Column = r.Metadata().Column
403+
pe.Message = fmt.Sprintf(`unable to convert reference %s to context variable: %s`, v, err)
404+
pe.Level = errors.ParserErrorLevelError
405+
406+
return pe
407+
}
408+
409+
// remove the attributes and to get a pure resource ref
410+
fqrn.Attribute = ""
411+
412+
err = setContextVariableFromPath(ctx, fqrn.String(), ctyRes)
413+
if err != nil {
414+
pe := &errors.ParserError{}
415+
pe.Filename = r.Metadata().File
416+
pe.Line = r.Metadata().Line
417+
pe.Column = r.Metadata().Column
418+
pe.Message = fmt.Sprintf(`unable to set context variable: %s`, err)
419+
pe.Level = errors.ParserErrorLevelError
420+
421+
return pe
422+
}
423+
}
424+
425+
return nil
426+
}

parse_test.go

+54-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package hclconfig
22

33
import (
44
"fmt"
5+
"log"
56
"os"
67
"path/filepath"
78
"sync"
@@ -382,7 +383,7 @@ func TestParseModuleCreatesResources(t *testing.T) {
382383
c, err := p.ParseFile(absoluteFolderPath)
383384
require.NoError(t, err)
384385

385-
require.Len(t, c.Resources, 35)
386+
require.Len(t, c.Resources, 39)
386387

387388
// check resource has been created
388389
cont, err := c.FindResource("module.consul_1.resource.container.consul")
@@ -436,7 +437,7 @@ func TestParseModuleCreatesOutputs(t *testing.T) {
436437
c, err := p.ParseFile(absoluteFolderPath)
437438
require.NoError(t, err)
438439

439-
require.Len(t, c.Resources, 35)
440+
require.Len(t, c.Resources, 39)
440441

441442
cont, err := c.FindResource("output.module1_container_resources_cpu")
442443
require.NoError(t, err)
@@ -510,6 +511,54 @@ func TestDoesNotLoadsVariablesFilesFromInsideModules(t *testing.T) {
510511
require.Equal(t, 2048, cont.Resources.CPU)
511512
}
512513

514+
func TestModuleDisabledCanBeOverriden(t *testing.T) {
515+
absoluteFolderPath, err := filepath.Abs("./test_fixtures/modules/modules.hcl")
516+
if err != nil {
517+
t.Fatal(err)
518+
}
519+
520+
callbackMutext := sync.Mutex{}
521+
522+
calls := []string{}
523+
o := DefaultOptions()
524+
o.Callback = func(r types.Resource) error {
525+
callbackMutext.Lock()
526+
log.Printf("callback: %s", r.Metadata().ID)
527+
calls = append(calls, r.Metadata().ID)
528+
529+
callbackMutext.Unlock()
530+
531+
return nil
532+
}
533+
534+
p := setupParser(t, o)
535+
536+
c, err := p.ParseFile(absoluteFolderPath)
537+
require.NoError(t, err)
538+
539+
// test disabled overrides are set
540+
r, err := c.FindResource("module.consul_2.resource.container.sidecar")
541+
require.NoError(t, err)
542+
543+
// check disabled has been interpolated
544+
cont := r.(*structs.Container)
545+
require.False(t, cont.Disabled)
546+
547+
// check that the module resources callbacks are called
548+
require.Contains(t, calls, "module.consul_2.resource.container.sidecar")
549+
550+
// test disabled is maintainerd
551+
r, err = c.FindResource("module.consul_1.resource.container.sidecar")
552+
require.NoError(t, err)
553+
554+
// check disabled has been interpolated
555+
cont = r.(*structs.Container)
556+
require.True(t, cont.Disabled)
557+
558+
// check that the module resources callbacks are called
559+
require.NotContains(t, calls, "module.consul_1.resource.container.sidecar")
560+
}
561+
513562
func TestParseContainerWithNoNameReturnsError(t *testing.T) {
514563
absoluteFolderPath, err := filepath.Abs("./test_fixtures/invalid/no_name.hcl")
515564
if err != nil {
@@ -567,7 +616,7 @@ func TestParseDoesNotProcessDisabledResources(t *testing.T) {
567616

568617
c, err := p.ParseFile(absoluteFolderPath)
569618
require.NoError(t, err)
570-
require.Equal(t, 3, c.ResourceCount())
619+
require.Equal(t, 4, c.ResourceCount())
571620

572621
r, err := c.FindResource("resource.container.disabled_value")
573622
require.NoError(t, err)
@@ -821,8 +870,8 @@ func TestParserStopsParseOnCallbackError(t *testing.T) {
821870
_, err = p.ParseFile(absoluteFolderPath)
822871
require.Error(t, err)
823872

824-
// only 13 of the resources and variables should be created, none of the descendants of base
825-
require.Len(t, calls, 13)
873+
// only 16 of the resources and variables should be created, none of the descendants of base
874+
require.Len(t, calls, 16)
826875
require.NotContains(t, "resource.module.consul_1", calls)
827876
}
828877

0 commit comments

Comments
 (0)