Skip to content

Commit ce2ef11

Browse files
authored
feat(be): support args for terraform apps (#3400)
* feat(be): support args for terraform apps * test(config): add additional unit tests for map ot struct * refactor(tf): check app type in makeCmd method * fix(tf): args order
1 parent 387e212 commit ce2ef11

File tree

5 files changed

+297
-18
lines changed

5 files changed

+297
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ api/public/**/*
1919
/.dredd/config.json
2020
/database.boltdb
2121
/database.sqlite
22+
/database.sqlite-journal
2223
/database.boltdb.lock
2324
/database_test.boltdb
2425
.DS_Store

db/config_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package db
22

33
import (
4-
"github.com/semaphoreui/semaphore/util"
54
"testing"
5+
6+
"github.com/semaphoreui/semaphore/util"
67
)
78

89
func TestConfig_assignMapToStruct(t *testing.T) {
@@ -22,6 +23,7 @@ func TestConfig_assignMapToStruct(t *testing.T) {
2223
Email string `json:"email"`
2324
Address Address `json:"address"`
2425
Details map[string]Detail `json:"details"`
26+
Tags []string `json:"tags"`
2527
}
2628

2729
johnData := map[string]any{
@@ -45,6 +47,7 @@ func TestConfig_assignMapToStruct(t *testing.T) {
4547
"description": "Ho ho ho",
4648
},
4749
},
50+
"tags": "[\"test\"]",
4851
}
4952

5053
var john User
@@ -72,6 +75,10 @@ func TestConfig_assignMapToStruct(t *testing.T) {
7275
t.Errorf("Expected interests to be politics but got '%s'", john.Details["interests"].Value)
7376
}
7477

78+
if len(john.Tags) < 1 {
79+
t.Fatal("Expected user tags")
80+
}
81+
7582
//if john.Details["occupation"].Value != "engineer" {
7683
// t.Errorf("Expected occupation to be engineer but got %s", john.Details["occupation"].Value)
7784
//}

db_lib/TerraformApp.go

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,30 @@ func (r *terraformReader) Read(p []byte) (n int, err error) {
7070
}
7171

7272
func (t *TerraformApp) makeCmd(command string, args []string, environmentVars []string) *exec.Cmd {
73+
74+
if app, ok := util.Config.Apps[t.Name]; ok {
75+
if app.AppPath != "" {
76+
command = app.AppPath
77+
}
78+
if app.AppArgs != nil {
79+
args = append(app.AppArgs, args...)
80+
}
81+
}
82+
83+
if t.Name == string(db.AppTerragrunt) {
84+
hasTfPath := false
85+
for i := 0; i < len(args); i++ {
86+
a := args[i]
87+
if a == "--tf-path" || strings.HasPrefix(a, "--tf-path=") {
88+
hasTfPath = true
89+
break
90+
}
91+
}
92+
if !hasTfPath {
93+
args = append(args, "--tf-path=terraform")
94+
}
95+
}
96+
7397
cmd := exec.Command(command, args...) //nolint: gas
7498
cmd.Dir = t.GetFullPath()
7599

@@ -126,10 +150,6 @@ func (t *TerraformApp) init(environmentVars []string, keyInstaller AccessKeyInst
126150
args = append(args, "-migrate-state")
127151
}
128152

129-
if t.Name == string(db.AppTerragrunt) {
130-
args = append(args, "--tf-path=terraform")
131-
}
132-
133153
cmd := t.makeCmd(t.Name, args, environmentVars)
134154
cmd.Env = append(cmd.Env, keyInstallation.GetGitEnv()...)
135155
t.Logger.LogCmd(cmd)
@@ -161,10 +181,7 @@ func (t *TerraformApp) init(environmentVars []string, keyInstaller AccessKeyInst
161181

162182
func (t *TerraformApp) isWorkspacesSupported(environmentVars []string) bool {
163183
args := []string{"workspace", "list"}
164-
if t.Name == string(db.AppTerragrunt) {
165-
args = append([]string{"run", "--"}, args...)
166-
args = append(args, "--tf-path=terraform")
167-
}
184+
168185
cmd := t.makeCmd(t.Name, args, environmentVars)
169186
err := cmd.Run()
170187
if err != nil {
@@ -178,7 +195,6 @@ func (t *TerraformApp) selectWorkspace(workspace string, environmentVars []strin
178195
args := []string{"workspace", "select", "-or-create=true", workspace}
179196
if t.Name == string(db.AppTerragrunt) {
180197
args = append([]string{"run", "--"}, args...)
181-
args = append(args, "--tf-path=terraform")
182198
}
183199
cmd := t.makeCmd(t.Name, args, environmentVars)
184200
t.Logger.LogCmd(cmd)
@@ -252,9 +268,6 @@ func (t *TerraformApp) InstallRequirements(args LocalAppInstallingArgs) (err err
252268

253269
func (t *TerraformApp) Plan(args []string, environmentVars []string, inputs map[string]string, cb func(*os.Process)) error {
254270
planArgs := []string{"plan", "-lock=false"}
255-
if t.Name == string(db.AppTerragrunt) {
256-
planArgs = append(planArgs, "--tf-path=terraform")
257-
}
258271
planArgs = append(planArgs, args...)
259272
cmd := t.makeCmd(t.Name, planArgs, environmentVars)
260273
t.Logger.LogCmd(cmd)
@@ -284,9 +297,6 @@ func (t *TerraformApp) Plan(args []string, environmentVars []string, inputs map[
284297

285298
func (t *TerraformApp) Apply(args []string, environmentVars []string, inputs map[string]string, cb func(*os.Process)) error {
286299
applyArgs := []string{"apply", "-auto-approve", "-lock=false"}
287-
if t.Name == string(db.AppTerragrunt) {
288-
applyArgs = append(applyArgs, "--tf-path=terraform")
289-
}
290300
applyArgs = append(applyArgs, args...)
291301
cmd := t.makeCmd(t.Name, applyArgs, environmentVars)
292302
t.Logger.LogCmd(cmd)

util/config.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,74 @@ func assignMapToStructRecursive(m map[string]any, structValue reflect.Value) err
623623
if err != nil {
624624
return err
625625
}
626+
case reflect.Slice:
627+
// Handle slice assignment
628+
fieldElemType := fieldValue.Type().Elem()
629+
var sourceSlice reflect.Value
630+
if val.Kind() == reflect.Slice || val.Kind() == reflect.Array {
631+
sourceSlice = val
632+
} else if val.Kind() == reflect.String {
633+
// Try to parse JSON array from string
634+
str := val.String()
635+
// First, try to unmarshal into []any
636+
var anyArr []any
637+
if err := json.Unmarshal([]byte(str), &anyArr); err == nil {
638+
sourceSlice = reflect.ValueOf(anyArr)
639+
} else if fieldElemType.Kind() == reflect.String {
640+
// Fallback: treat as single element string
641+
sourceSlice = reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf("")), 1, 1)
642+
sourceSlice.Index(0).SetString(str)
643+
} else {
644+
return fmt.Errorf("expected slice or json array string for field %s but got %T", field.Name, value)
645+
}
646+
} else {
647+
return fmt.Errorf("expected slice for field %s but got %T", field.Name, value)
648+
}
649+
650+
// Build destination slice
651+
newSlice := reflect.MakeSlice(fieldValue.Type(), 0, sourceSlice.Len())
652+
for i := 0; i < sourceSlice.Len(); i++ {
653+
srcElemVal := sourceSlice.Index(i)
654+
// When source is []any, elements come as interface{}, unwrap reflect.Value
655+
if srcElemVal.Kind() == reflect.Interface && !srcElemVal.IsNil() {
656+
srcElemVal = reflect.ValueOf(srcElemVal.Interface())
657+
}
658+
659+
var dstElem reflect.Value
660+
// Prepare destination element
661+
if fieldElemType.Kind() == reflect.Struct {
662+
dstElem = reflect.New(fieldElemType).Elem()
663+
if srcElemVal.Kind() == reflect.Map {
664+
// Expect map[string]any
665+
mIface, ok := srcElemVal.Interface().(map[string]any)
666+
if !ok {
667+
return fmt.Errorf("cannot assign element of type %T to slice element of type %s", srcElemVal.Interface(), fieldElemType)
668+
}
669+
if err := assignMapToStructRecursive(mIface, dstElem); err != nil {
670+
return err
671+
}
672+
} else if srcElemVal.Type().ConvertibleTo(fieldElemType) {
673+
dstElem = srcElemVal.Convert(fieldElemType)
674+
} else {
675+
return fmt.Errorf("cannot assign element of type %s to slice element of type %s", srcElemVal.Type(), fieldElemType)
676+
}
677+
} else {
678+
// Primitive or other kinds
679+
if srcElemVal.Type().ConvertibleTo(fieldElemType) {
680+
dstElem = srcElemVal.Convert(fieldElemType)
681+
} else {
682+
newVal, converted := CastValueToKind(srcElemVal.Interface(), fieldElemType.Kind())
683+
if !converted {
684+
return fmt.Errorf("cannot assign element of type %s to slice element of type %s", srcElemVal.Type(), fieldElemType)
685+
}
686+
dstElem = reflect.ValueOf(newVal)
687+
}
688+
}
689+
690+
newSlice = reflect.Append(newSlice, dstElem)
691+
}
692+
693+
fieldValue.Set(newSlice)
626694
case reflect.Map:
627695
if fieldValue.IsNil() {
628696
mapValue := reflect.MakeMap(fieldValue.Type())
@@ -693,14 +761,19 @@ func CastValueToKind(value any, kind reflect.Kind) (res any, ok bool) {
693761

694762
switch kind {
695763
case reflect.String:
764+
// strings are always acceptable as-is, or will be coerced upstream
696765
ok = true
697766
case reflect.Int:
698-
if reflect.ValueOf(value).Kind() != reflect.Int {
767+
if reflect.ValueOf(value).Kind() == reflect.Int {
768+
ok = true
769+
} else {
699770
res = castStringToInt(fmt.Sprintf("%v", reflect.ValueOf(value)))
700771
ok = true
701772
}
702773
case reflect.Bool:
703-
if reflect.ValueOf(value).Kind() != reflect.Bool {
774+
if reflect.ValueOf(value).Kind() == reflect.Bool {
775+
ok = true
776+
} else {
704777
res = castStringToBool(fmt.Sprintf("%v", reflect.ValueOf(value)))
705778
ok = true
706779
}

0 commit comments

Comments
 (0)