From 8d0ab4fafc825559f0bc9bbea2aac576df0dcf4c Mon Sep 17 00:00:00 2001 From: Darshil Parikh Date: Wed, 15 Jun 2022 16:34:57 -0700 Subject: [PATCH] fix(bind): interpret bind defined as type []map[string]string As per the type definition of Layer, bind is of type Bind which is a struct with fields Source and Dest. But when we do UnmarshalYAML for type Bind, we are assuming that we would always get the Bind as a []string and not []map[interface{}]interface{} (when making a call to getStringOrStringSlice). This PR fixes the getStringOrStringSlice to consider Bind defined as a []map[string]string and returns appropriate bind string in that case. Fixes https://github.com/project-stacker/stacker/issues/301 Signed-off-by: Darshil Parikh --- test/binds.bats | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ types/layer.go | 61 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 test/binds.bats diff --git a/test/binds.bats b/test/binds.bats new file mode 100644 index 00000000..fbab665f --- /dev/null +++ b/test/binds.bats @@ -0,0 +1,65 @@ +load helpers + +function setup() { + stacker_setup +} + +function teardown() { + cleanup +} + +@test "bind as string slice" { + cat > stacker.yaml <<"EOF" +bind-test: + from: + type: oci + url: ${{CENTOS_OCI}} + binds: + - ${{bind_path}} -> /root/tree1/foo + run: | + touch /root/tree1/foo/bar +EOF + mkdir -p tree1/foo + + # since we are creating directory as + # real root and then `touch`-ing a file + # where in user NS, need to have rw persmission + # for others + chmod +666 tree1/foo + + bind_path=$(realpath tree1/foo) + + out=$(stacker build --substitute bind_path=${bind_path} --substitute CENTOS_OCI=$CENTOS_OCI) + + [[ "${out}" =~ ^(.*filesystem bind-test built successfully)$ ]] + + stat tree1/foo/bar +} + +@test "bind as struct" { + cat > stacker.yaml <<"EOF" +bind-test: + from: + type: oci + url: ${{CENTOS_OCI}} + binds: + - Source: ${{bind_path}} + Dest: /root/tree1/foo + run: | + touch /root/tree1/foo/bar +EOF + mkdir -p tree1/foo + + # since we are creating directory as + # real root and then `touch`-ing a file + # where in user NS, need to have rw persmission + # for others + chmod +666 tree1/foo + + bind_path=$(realpath tree1/foo) + + out=$(stacker build --substitute bind_path=$bind_path --substitute CENTOS_OCI=$CENTOS_OCI) + [[ "${out}" =~ ^(.*filesystem bind-test built successfully)$ ]] + + stat tree1/foo/bar +} diff --git a/types/layer.go b/types/layer.go index 8747a036..688c9e93 100644 --- a/types/layer.go +++ b/types/layer.go @@ -44,9 +44,43 @@ type OverlayDir struct { type Imports []Import -func getStringOrStringSlice(iface interface{}, xform func(string) ([]string, error)) ([]string, error) { +func validateDataAsBind(i interface{}) (map[interface{}]interface{}, error) { + bindMap, ok := i.(map[interface{}]interface{}) + if !ok { + return nil, errors.Errorf("unable to cast into map[interface{}]interface{}: %T", i) + } + + // validations + bindSource, ok := bindMap["Source"] + if !ok { + return nil, errors.Errorf("bind source missing: %v", i) + } + + _, ok = bindSource.(string) + if !ok { + return nil, errors.Errorf("unknown bind source type, expected string: %T", i) + } + + bindDest, ok := bindMap["Dest"] + if !ok { + return nil, errors.Errorf("bind dest missing: %v", i) + } + + _, ok = bindDest.(string) + if !ok { + return nil, errors.Errorf("unknown bind dest type, expected string: %T", i) + } + + if bindSource == "" || bindDest == "" { + return nil, errors.Errorf("empty source or dest: %v", i) + } + + return bindMap, nil +} + +func getStringOrStringSlice(data interface{}, xform func(string) ([]string, error)) ([]string, error) { // The user didn't supply run: at all, so let's not do anything. - if iface == nil { + if data == nil { return []string{}, nil } @@ -54,12 +88,23 @@ func getStringOrStringSlice(iface interface{}, xform func(string) ([]string, err // run: // - foo // - bar - ifs, ok := iface.([]interface{}) + ifs, ok := data.([]interface{}) if ok { strs := []string{} for _, i := range ifs { - s, ok := i.(string) - if !ok { + s := "" + switch v := i.(type) { + case string: + s = v + case interface{}: + bindMap, err := validateDataAsBind(i) + if err != nil { + return nil, err + } + + // validations passed, return as string in form: source -> dest + s = fmt.Sprintf("%s -> %s", bindMap["Source"], bindMap["Dest"]) + default: return nil, errors.Errorf("unknown run array type: %T", i) } @@ -72,7 +117,7 @@ func getStringOrStringSlice(iface interface{}, xform func(string) ([]string, err // run: | // echo hello world // echo goodbye cruel world - line, ok := iface.(string) + line, ok := data.(string) if ok { return xform(line) } @@ -80,12 +125,12 @@ func getStringOrStringSlice(iface interface{}, xform func(string) ([]string, err // This is how it is after we do our find replace and re-set it; as a // convenience (so we don't have to re-wrap it in interface{}), let's // handle []string - strs, ok := iface.([]string) + strs, ok := data.([]string) if ok { return strs, nil } - return nil, errors.Errorf("unknown directive type: %T", iface) + return nil, errors.Errorf("unknown directive type: %T", data) } // StringList allows this type to be parsed from the yaml parser as either a