Skip to content

Commit 371c292

Browse files
authored
strict env format (#2641)
1 parent c849af4 commit 371c292

File tree

6 files changed

+148
-4
lines changed

6 files changed

+148
-4
lines changed

docs/user/gen-docs/kyma_app_push.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ kyma app push [flags]
2828

2929
## Push an application and set environment variables:
3030
# This flag overrides existing environment variables with the same name from other sources (file, ConfigMap, Secret).
31+
# To set an environment variable, use the format 'NAME=VALUE' or 'name=<NAME>,value=<VALUE>'.
3132
kyma app push --name my-app --code-path . --env NAME1=VALUE --env NAME2=VALUE2
3233

3334
## Push an application and set environment variables from different sources:

internal/cmd/app/push.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type appPushConfig struct {
3232
packAppPath string
3333
containerPort types.NullableInt64
3434
istioInject types.NullableBool
35-
envs types.Map
35+
envs types.EnvMap
3636
fileEnvs types.SourcedEnvArray
3737
configmapEnvs types.SourcedEnvArray
3838
secretEnvs types.SourcedEnvArray
@@ -45,7 +45,7 @@ type appPushConfig struct {
4545
func NewAppPushCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
4646
config := appPushConfig{
4747
KymaConfig: kymaConfig,
48-
envs: types.Map{Values: map[string]interface{}{}},
48+
envs: types.EnvMap{Map: &types.Map{Values: map[string]interface{}{}}},
4949
}
5050

5151
cmd := &cobra.Command{
@@ -67,6 +67,7 @@ func NewAppPushCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
6767
6868
## Push an application and set environment variables:
6969
# This flag overrides existing environment variables with the same name from other sources (file, ConfigMap, Secret).
70+
# To set an environment variable, use the format 'NAME=VALUE' or 'name=<NAME>,value=<VALUE>'.
7071
kyma app push --name my-app --code-path . --env NAME1=VALUE --env NAME2=VALUE2
7172
7273
## Push an application and set environment variables from different sources:

internal/cmdcommon/envs/env.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
corev1 "k8s.io/api/core/v1"
88
)
99

10-
func Build(envs types.Map) []corev1.EnvVar {
10+
func Build(envs types.EnvMap) []corev1.EnvVar {
1111
var result []corev1.EnvVar
1212
for k, v := range envs.Values {
1313
result = append(result, corev1.EnvVar{

internal/cmdcommon/types/env.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package types
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
var (
10+
ErrInvalidEnvFormat = errors.New("invalid env format, should be in format 'name=<NAME>,value=<VALUE>' or <NAME>=<VALUE>")
11+
ErrUnknownEnvField = errors.New("unknown env field, supported fields are 'name' or 'value'")
12+
)
13+
14+
type EnvMap struct {
15+
*Map
16+
}
17+
18+
// SetValue sets the value of the NullableString from a string
19+
func (e *EnvMap) SetValue(value *string) error {
20+
if value == nil {
21+
return nil
22+
}
23+
24+
if e.Map == nil || e.Values == nil {
25+
e.Map = &Map{Values: map[string]interface{}{}}
26+
}
27+
28+
if *value == "" {
29+
// input is empty, do nothing
30+
return nil
31+
}
32+
33+
fields := strings.SplitN(*value, ",", 2)
34+
if len(fields) == 2 {
35+
// input is in the format name=NAME,key=KEY
36+
envName, envValue, err := parseStrictEnv(fields)
37+
if err != nil {
38+
return fmt.Errorf("failed to parse value '%s', %w", *value, err)
39+
}
40+
e.Values[envName] = envValue
41+
return nil
42+
}
43+
44+
// input is in the format NAME=VALUE
45+
envElems := strings.Split(fields[0], "=")
46+
if len(envElems) != 2 {
47+
return fmt.Errorf("failed to parse value '%s': %w", *value, ErrInvalidEnvFormat)
48+
}
49+
e.Values[envElems[0]] = envElems[1]
50+
return nil
51+
}
52+
53+
// parseStrictEnv parses env in strict format (like 'name=<NAME>,path=<PATH>')
54+
// returns env name on first elem and its value on second elem
55+
func parseStrictEnv(fields []string) (string, string, error) {
56+
envName := ""
57+
envValue := ""
58+
for _, v := range fields {
59+
elems := strings.SplitN(v, "=", 2)
60+
if len(elems) != 2 {
61+
// invalid format, '=' not found
62+
return "", "", ErrInvalidEnvFormat
63+
}
64+
65+
switch elems[0] {
66+
case "name":
67+
envName = elems[1]
68+
case "value":
69+
envValue = elems[1]
70+
default:
71+
return "", "", ErrUnknownEnvField
72+
}
73+
}
74+
75+
return envName, envValue, nil
76+
}
77+
78+
// Set implements the flag.Value interface
79+
func (e *EnvMap) Set(value string) error {
80+
if value == "" {
81+
return nil
82+
}
83+
84+
return e.SetValue(&value)
85+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package types_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/kyma-project/cli.v3/internal/cmdcommon/types"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestEnvMap_Set(t *testing.T) {
11+
t.Run("should set env map value", func(t *testing.T) {
12+
envMap := &types.EnvMap{}
13+
err := envMap.Set("MY_ENV=my_value")
14+
require.NoError(t, err)
15+
err = envMap.Set("name=ANOTHER_ENV,value=another_value")
16+
require.NoError(t, err)
17+
18+
require.Equal(t, map[string]interface{}{
19+
"MY_ENV": "my_value",
20+
"ANOTHER_ENV": "another_value",
21+
}, envMap.Values)
22+
})
23+
24+
t.Run("should return error on invalid format", func(t *testing.T) {
25+
envMap := &types.EnvMap{Map: &types.Map{Values: map[string]interface{}{}}}
26+
err := envMap.Set("invalid_format")
27+
require.ErrorIs(t, err, types.ErrInvalidEnvFormat)
28+
})
29+
30+
t.Run("should return error on unknown field", func(t *testing.T) {
31+
envMap := &types.EnvMap{Map: &types.Map{Values: map[string]interface{}{}}}
32+
err := envMap.Set("name=MY_ENV,unknown_field=value")
33+
require.ErrorIs(t, err, types.ErrUnknownEnvField)
34+
})
35+
36+
t.Run("should return error on empty name", func(t *testing.T) {
37+
envMap := &types.EnvMap{Map: &types.Map{Values: map[string]interface{}{}}}
38+
err := envMap.Set("value=my_value,name")
39+
require.ErrorIs(t, err, types.ErrInvalidEnvFormat)
40+
})
41+
42+
t.Run("should ignore empty value", func(t *testing.T) {
43+
envMap := &types.EnvMap{Map: &types.Map{Values: map[string]interface{}{}}}
44+
err := envMap.Set("")
45+
require.NoError(t, err)
46+
require.Empty(t, envMap.Values)
47+
})
48+
49+
t.Run("should ignore nil value", func(t *testing.T) {
50+
envMap := &types.EnvMap{Map: &types.Map{Values: map[string]interface{}{}}}
51+
err := envMap.SetValue(nil)
52+
require.NoError(t, err)
53+
require.Empty(t, envMap.Values)
54+
})
55+
}

internal/cmdcommon/types/sourced/env.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ func (e *Env) String() string {
3939

4040
func ParseEnv(value string) (Env, error) {
4141
values := strings.Split(value, ",")
42-
if len(values) > 1 || strings.HasPrefix(values[0], "path=") {
42+
if len(values) > 1 ||
43+
strings.HasPrefix(values[0], "resource=") ||
44+
strings.HasPrefix(values[0], "path=") {
4345
// strict format (like 'name=<NAME>,path=<PATH>')
4446
return parseStrictEnv(values)
4547
}

0 commit comments

Comments
 (0)