Repository: compose-spec/compose-go
Pull request #237 (2022-03-10 11:53:57)

Move stringutils upstream
Signed-off-by: Laura Brehm <laurabrehm@hey.com>

As discussed with @ulyssessouza, moving this upstream so we can refactor some of the code and use it here as well. I'll make another PR to the downstream to address the changes.


Patch:
diff --git a/loader/full-example.yml b/loader/full-example.yml
index 4f17450..71e7820 100644
--- a/loader/full-example.yml
+++ b/loader/full-example.yml
@@ -7,8 +7,6 @@ services:
       dockerfile: Dockerfile
       args:
         foo: bar
-      ssh:
-        - default
       target: foo
       network: foo
       cache_from:
diff --git a/loader/loader.go b/loader/loader.go
index 5aeeb21..8394287 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -390,7 +390,6 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec
 		reflect.TypeOf(types.DependsOnConfig{}):                  transformDependsOnConfig,
 		reflect.TypeOf(types.ExtendsConfig{}):                    transformExtendsConfig,
 		reflect.TypeOf(types.DeviceRequest{}):                    transformServiceDeviceRequest,
-		reflect.TypeOf(types.SSHConfig{}):                        transformSSHConfig,
 	}
 
 	for _, transformer := range additionalTransformers {
@@ -979,35 +978,6 @@ var transformServiceNetworkMap TransformerFunc = func(value interface{}) (interf
 	return value, nil
 }
 
-var transformSSHConfig TransformerFunc = func(data interface{}) (interface{}, error) {
-	switch value := data.(type) {
-	case map[string]interface{}:
-		var result []types.SSHKey
-		for key, val := range value {
-			if val == nil {
-				val = ""
-			}
-			result = append(result, types.SSHKey{ID: key, Path: val.(string)})
-		}
-		return result, nil
-	case []interface{}:
-		var result []types.SSHKey
-		for _, v := range value {
-			key, val := transformValueToMapEntry(v.(string), "=", false)
-			result = append(result, types.SSHKey{ID: key, Path: val.(string)})
-		}
-		return result, nil
-	case string:
-		if value == "" {
-			value = "default"
-		}
-		key, val := transformValueToMapEntry(value, "=", false)
-		result := []types.SSHKey{{ID: key, Path: val.(string)}}
-		return result, nil
-	}
-	return nil, errors.Errorf("expected a sting, map or a list, got %T: %#v", data, data)
-}
-
 var transformStringOrNumberList TransformerFunc = func(value interface{}) (interface{}, error) {
 	list := value.([]interface{})
 	result := make([]string, len(list))
@@ -1030,52 +1000,47 @@ var transformStringList TransformerFunc = func(data interface{}) (interface{}, e
 
 func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc {
 	return func(data interface{}) (interface{}, error) {
-		return transformMappingOrList(data, sep, allowNil)
+		return transformMappingOrList(data, sep, allowNil), nil
 	}
 }
 
 func transformListOrMappingFunc(sep string, allowNil bool) TransformerFunc {
 	return func(data interface{}) (interface{}, error) {
-		return transformListOrMapping(data, sep, allowNil)
+		return transformListOrMapping(data, sep, allowNil), nil
 	}
 }
 
-func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) (interface{}, error) {
+func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} {
 	switch value := listOrMapping.(type) {
 	case map[string]interface{}:
-		return toStringList(value, sep, allowNil), nil
+		return toStringList(value, sep, allowNil)
 	case []interface{}:
-		return listOrMapping, nil
+		return listOrMapping
 	}
-	return nil, errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping)
+	panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
 }
 
-func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) (interface{}, error) {
+func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} {
 	switch value := mappingOrList.(type) {
 	case map[string]interface{}:
-		return toMapStringString(value, allowNil), nil
+		return toMapStringString(value, allowNil)
 	case []interface{}:
 		result := make(map[string]interface{})
 		for _, value := range value {
-			key, val := transformValueToMapEntry(value.(string), sep, allowNil)
-			result[key] = val
+			parts := strings.SplitN(value.(string), sep, 2)
+			key := parts[0]
+			switch {
+			case len(parts) == 1 && allowNil:
+				result[key] = nil
+			case len(parts) == 1 && !allowNil:
+				result[key] = ""
+			default:
+				result[key] = parts[1]
+			}
 		}
-		return result, nil
-	}
-	return nil, errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)
-}
-
-func transformValueToMapEntry(value string, separator string, allowNil bool) (string, interface{}) {
-	parts := strings.SplitN(value, separator, 2)
-	key := parts[0]
-	switch {
-	case len(parts) == 1 && allowNil:
-		return key, nil
-	case len(parts) == 1 && !allowNil:
-		return key, ""
-	default:
-		return key, parts[1]
+		return result
 	}
+	panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
 }
 
 var transformShellCommand TransformerFunc = func(value interface{}) (interface{}, error) {
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 5bf4ddd..91a3473 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -91,7 +91,6 @@
                 "context": {"type": "string"},
                 "dockerfile": {"type": "string"},
                 "args": {"$ref": "#/definitions/list_or_dict"},
-                "ssh": {"$ref": "#/definitions/list_or_dict"},
                 "labels": {"$ref": "#/definitions/list_or_dict"},
                 "cache_from": {"type": "array", "items": {"type": "string"}},
                 "cache_to": {"type": "array", "items": {"type": "string"}},
diff --git a/types/types.go b/types/types.go
index e25150e..f23c114 100644
--- a/types/types.go
+++ b/types/types.go
@@ -294,7 +294,6 @@ type BuildConfig struct {
 	Context    string            `yaml:",omitempty" json:"context,omitempty"`
 	Dockerfile string            `yaml:",omitempty" json:"dockerfile,omitempty"`
 	Args       MappingWithEquals `yaml:",omitempty" json:"args,omitempty"`
-	SSH        SSHConfig         `yaml:"ssh,omitempty" json:"ssh,omitempty"`
 	Labels     Labels            `yaml:",omitempty" json:"labels,omitempty"`
 	CacheFrom  StringList        `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"`
 	CacheTo    StringList        `mapstructure:"cache_to" yaml:"cache_to,omitempty" json:"cache_to,omitempty"`
@@ -427,39 +426,6 @@ func (l Labels) Add(key, value string) Labels {
 	return l
 }
 
-type SSHKey struct {
-	ID   string
-	Path string
-}
-
-// SSHConfig is a mapping type for SSH build config
-type SSHConfig []SSHKey
-
-func (s SSHConfig) Get(id string) (string, error) {
-	for _, sshKey := range s {
-		if sshKey.ID == id {
-			return sshKey.Path, nil
-		}
-	}
-	return "", fmt.Errorf("ID %s not found in SSH keys", id)
-}
-
-// MarshalYAML makes SSHKey implement yaml.Marshaller
-func (s SSHKey) MarshalYAML() (interface{}, error) {
-	if s.Path == "" {
-		return s.ID, nil
-	}
-	return fmt.Sprintf("%s: %s", s.ID, s.Path), nil
-}
-
-// MarshalJSON makes SSHKey implement json.Marshaller
-func (s SSHKey) MarshalJSON() ([]byte, error) {
-	if s.Path == "" {
-		return []byte(fmt.Sprintf(`"%s"`, s.ID)), nil
-	}
-	return []byte(fmt.Sprintf(`"%s": %s`, s.ID, s.Path)), nil
-}
-
 // MappingWithColon is a mapping type that can be converted from a list of
 // 'key: value' strings
 type MappingWithColon map[string]string
diff --git a/utils/stringutils.go b/utils/stringutils.go
new file mode 100644
index 0000000..bd95e7b
--- /dev/null
+++ b/utils/stringutils.go
@@ -0,0 +1,38 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package utils
+
+import (
+	"strconv"
+	"strings"
+)
+
+// StringContains check if an array contains a specific value
+func StringContains(array []string, needle string) bool {
+	for _, val := range array {
+		if val == needle {
+			return true
+		}
+	}
+	return false
+}
+
+// StringToBool converts a string to a boolean ignoring errors
+func StringToBool(s string) bool {
+	b, _ := strconv.ParseBool(strings.ToLower(strings.TrimSpace(s)))
+	return b
+}


Test patch:
diff --git a/loader/full-struct_test.go b/loader/full-struct_test.go
index ee19fd2..8a1623e 100644
--- a/loader/full-struct_test.go
+++ b/loader/full-struct_test.go
@@ -53,7 +53,6 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
 				Context:    "./dir",
 				Dockerfile: "Dockerfile",
 				Args:       map[string]*string{"foo": strPtr("bar")},
-				SSH:        []types.SSHKey{{ID: "default", Path: ""}},
 				Target:     "foo",
 				Network:    "foo",
 				CacheFrom:  []string{"foo", "bar"},
@@ -570,8 +569,6 @@ services:
       dockerfile: Dockerfile
       args:
         foo: bar
-      ssh:
-      - default
       labels:
         FOO: BAR
       cache_from:
@@ -1085,9 +1082,6 @@ func fullExampleJSON(workingDir, homeDir string) string {
         "args": {
           "foo": "bar"
         },
-        "ssh": [
-          "default"
-        ],
         "labels": {
           "FOO": "BAR"
         },
diff --git a/loader/loader_test.go b/loader/loader_test.go
index a4106c8..78625be 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -1867,83 +1867,3 @@ func TestLoadServiceWithVolumes(t *testing.T) {
 	assert.Equal(t, len(s.Secrets), 1)
 	assert.Equal(t, "/path 4", s.Secrets[0].Target)
 }
-
-func TestLoadNoSSHInBuildConfig(t *testing.T) {
-	actual, err := loadYAML(`
-services:
-  test:
-    build:
-      context: .
-`)
-	assert.NilError(t, err)
-	svc, err := actual.GetService("test")
-	assert.NilError(t, err)
-	assert.Check(t, nil == svc.Build.SSH)
-}
-
-func TestLoadSSHWithoutValueInBuildConfig(t *testing.T) {
-	_, err := loadYAML(`
-services:
-  test:
-    build:
-      context: .
-      ssh:
-`)
-	assert.ErrorContains(t, err, "services.test.build.ssh must be a mapping")
-}
-
-func TestLoadSSHWithDefaultValueInBuildConfig(t *testing.T) {
-	actual, err := loadYAML(`
-services:
-  test:
-    build:
-      context: .
-      ssh: [default]
-`)
-	assert.NilError(t, err)
-	svc, err := actual.GetService("test")
-	assert.NilError(t, err)
-	sshValue, err := svc.Build.SSH.Get("default")
-	assert.NilError(t, err)
-	assert.Equal(t, "", sshValue)
-}
-
-func TestLoadSSHWithKeyValueInBuildConfig(t *testing.T) {
-	actual, err := loadYAML(`
-services:
-  test:
-    build:
-      context: .
-      ssh: 
-        key1: value1
-`)
-	assert.NilError(t, err)
-	svc, err := actual.GetService("test")
-	assert.NilError(t, err)
-	sshValue, err := svc.Build.SSH.Get("key1")
-	assert.NilError(t, err)
-	assert.Equal(t, "value1", sshValue)
-}
-
-func TestLoadSSHWithKeysValuesInBuildConfig(t *testing.T) {
-	actual, err := loadYAML(`
-services:
-  test:
-    build:
-      context: .
-      ssh: 
-        - key1=value1
-        - key2=value2
-`)
-	assert.NilError(t, err)
-	svc, err := actual.GetService("test")
-	assert.NilError(t, err)
-
-	sshValue, err := svc.Build.SSH.Get("key1")
-	assert.NilError(t, err)
-	assert.Equal(t, "value1", sshValue)
-
-	sshValue, err = svc.Build.SSH.Get("key2")
-	assert.NilError(t, err)
-	assert.Equal(t, "value2", sshValue)
-}