diff --git a/api/util/apps.go b/api/util/apps.go index 5e7e1869..f7fc8cad 100644 --- a/api/util/apps.go +++ b/api/util/apps.go @@ -73,16 +73,28 @@ func VerifiedAppHosts(app *apps.Application) []string { return verifiedHosts } -func EnvVarsFromMap(env map[string]string) apps.EnvVars { +type EnvVarModifier func(envVar *apps.EnvVar) + +func EnvVarsFromMap(env map[string]string, options ...EnvVarModifier) apps.EnvVars { vars := apps.EnvVars{} for k, v := range env { - vars = append(vars, apps.EnvVar{Name: k, Value: v}) + envVar := apps.EnvVar{Name: k, Value: v} + for _, opt := range options { + opt(&envVar) + } + vars = append(vars, envVar) } return vars } -func UpdateEnvVars(oldEnvs []apps.EnvVar, newEnvs map[string]string, toDelete []string) apps.EnvVars { - if len(newEnvs) == 0 && len(toDelete) == 0 { +func Sensitive() EnvVarModifier { + return func(envVar *apps.EnvVar) { + envVar.Sensitive = ptr.To(true) + } +} + +func UpdateEnvVars(oldEnvs []apps.EnvVar, newEnvs, sensitiveEnvs map[string]string, toDelete []string) apps.EnvVars { + if len(newEnvs) == 0 && len(sensitiveEnvs) == 0 && len(toDelete) == 0 { return oldEnvs } @@ -95,6 +107,10 @@ func UpdateEnvVars(oldEnvs []apps.EnvVar, newEnvs map[string]string, toDelete [] for _, v := range new { envMap[v.Name] = v } + sensitive := EnvVarsFromMap(sensitiveEnvs, Sensitive()) + for _, v := range sensitive { + envMap[v.Name] = v + } for _, v := range toDelete { delete(envMap, v) @@ -123,7 +139,11 @@ func EnvVarToString(envs apps.EnvVars) string { var keyValuePairs []string for _, env := range envs { - keyValuePairs = append(keyValuePairs, fmt.Sprintf("%v=%v", env.Name, env.Value)) + if env.Sensitive != nil && *env.Sensitive { + keyValuePairs = append(keyValuePairs, fmt.Sprintf("%v=*****", env.Name)) + } else { + keyValuePairs = append(keyValuePairs, fmt.Sprintf("%v=%v", env.Name, env.Value)) + } } return strings.Join(keyValuePairs, ";") diff --git a/api/util/apps_test.go b/api/util/apps_test.go index 0bb1ed05..869e35b3 100644 --- a/api/util/apps_test.go +++ b/api/util/apps_test.go @@ -25,7 +25,7 @@ func TestEnvUpdate(t *testing.T) { } up := map[string]string{"old2": "val2"} del := []string{"old3"} - new := UpdateEnvVars(old, up, del) + new := UpdateEnvVars(old, up, nil, del) expected := apps.EnvVars{ { Name: "old1", diff --git a/create/application.go b/create/application.go index 4231d3ff..fadda414 100644 --- a/create/application.go +++ b/create/application.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "slices" "strconv" "strings" "time" @@ -42,7 +43,9 @@ type applicationCmd struct { Hosts []string `help:"Host names where the app can be accessed. If empty, the app will just be accessible on a generated host name on the deploio.app domain."` BasicAuth *bool `help:"Enable/Disable basic authentication for the app (defaults to ${app_default_basic_auth})." placeholder:"${app_default_basic_auth}"` Env map[string]string `help:"Environment variables which are passed to the app at runtime."` + SensitiveEnv map[string]string `help:"Sensitive environment variables which are passed to the app at runtime."` BuildEnv map[string]string `help:"Environment variables which are passed to the app build process."` + SensitiveBuildEnv map[string]string `help:"Sensitive environment variables which are passed to the app build process."` DeployJob deployJob `embed:"" prefix:"deploy-job-"` WorkerJob workerJob `embed:"" prefix:"worker-job-"` ScheduledJob scheduledJob `embed:"" prefix:"scheduled-job-"` @@ -273,6 +276,13 @@ func spinnerMessage(msg, icon string, sleepTime time.Duration) error { return spinner.Stop() } +func combineEnvVars(plain, sensitive map[string]string) apps.EnvVars { + return slices.Concat( + util.EnvVarsFromMap(plain), + util.EnvVarsFromMap(sensitive, util.Sensitive()), + ) +} + func (app *applicationCmd) config() apps.Config { var deployJob *apps.DeployJob @@ -288,10 +298,9 @@ func (app *applicationCmd) config() apps.Config { }, } } - config := apps.Config{ EnableBasicAuth: app.BasicAuth, - Env: util.EnvVarsFromMap(app.Env), + Env: combineEnvVars(app.Env, app.SensitiveEnv), DeployJob: deployJob, } @@ -358,7 +367,7 @@ func (app *applicationCmd) newApplication(project string) *apps.Application { }, Hosts: app.Hosts, Config: app.config(), - BuildEnv: util.EnvVarsFromMap(app.BuildEnv), + BuildEnv: combineEnvVars(app.BuildEnv, app.SensitiveBuildEnv), DockerfileBuild: apps.DockerfileBuild{ Enabled: app.DockerfileBuild.Enabled, DockerfilePath: app.DockerfileBuild.Path, diff --git a/create/application_test.go b/create/application_test.go index 79ab5786..9a3d7040 100644 --- a/create/application_test.go +++ b/create/application_test.go @@ -440,6 +440,34 @@ func TestApplication(t *testing.T) { assert.Nil(t, app.Spec.ForProvider.Git.Auth) }, }, + "with sensitive env": { + cmd: applicationCmd{ + resourceCmd: resourceCmd{ + Name: "sensitive-env-test", + }, + Git: gitConfig{ + URL: "https://github.com/ninech/doesnotexist.git", + SubPath: "/my/app", + Revision: "superbug", + }, + SensitiveEnv: map[string]string{"secret": "orange"}, + SensitiveBuildEnv: map[string]string{"build_secret": "banana"}, + SkipRepoAccessCheck: true, + }, + checkApp: func(t *testing.T, cmd applicationCmd, app *apps.Application) { + env := util.EnvVarByName(app.Spec.ForProvider.Config.Env, "secret") + require.NotNil(t, env) + require.NotNil(t, env.Sensitive) + assert.True(t, *env.Sensitive) + assert.Equal(t, "orange", env.Value) + + buildEnv := util.EnvVarByName(app.Spec.ForProvider.BuildEnv, "build_secret") + require.NotNil(t, buildEnv) + require.NotNil(t, buildEnv.Sensitive) + assert.True(t, *buildEnv.Sensitive) + assert.Equal(t, "banana", buildEnv.Value) + }, + }, } for name, tc := range cases { diff --git a/get/project_config_test.go b/get/project_config_test.go index 78019645..3e4d392a 100644 --- a/get/project_config_test.go +++ b/get/project_config_test.go @@ -68,6 +68,58 @@ func TestProjectConfigs(t *testing.T) { project: "ns-3", expectExactMessage: ptr.To("no ProjectConfigs found in project ns-3\n"), }, + "sensitive env var is masked": { + get: &Cmd{ + output: output{ + Format: full, + }, + }, + project: "ns-4", + createdConfigs: []client.Object{ + &apps.ProjectConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-4", + Namespace: "ns-4", + }, + Spec: apps.ProjectConfigSpec{ + ForProvider: apps.ProjectConfigParameters{ + Config: apps.Config{ + Env: util.EnvVarsFromMap(map[string]string{"poo": "orange"}, util.Sensitive()), + }, + }, + }, + }, + }, + expectExactMessage: ptr.To( + "PROJECT NAME SIZE REPLICAS PORT ENVIRONMENT_VARIABLES BASIC_AUTH DEPLOY_JOB AGE\nns-4 ns-4 poo=***** false 292y\n", + ), + }, + "non-sensitive env var is shown": { + get: &Cmd{ + output: output{ + Format: full, + }, + }, + project: "ns-5", + createdConfigs: []client.Object{ + &apps.ProjectConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-5", + Namespace: "ns-5", + }, + Spec: apps.ProjectConfigSpec{ + ForProvider: apps.ProjectConfigParameters{ + Config: apps.Config{ + Env: util.EnvVarsFromMap(map[string]string{"goo": "banana"}), + }, + }, + }, + }, + }, + expectExactMessage: ptr.To( + "PROJECT NAME SIZE REPLICAS PORT ENVIRONMENT_VARIABLES BASIC_AUTH DEPLOY_JOB AGE\nns-5 ns-5 goo=banana false 292y\n", + ), + }, } for name, tc := range cases { diff --git a/update/application.go b/update/application.go index 119d6174..c1a7310a 100644 --- a/update/application.go +++ b/update/application.go @@ -35,9 +35,13 @@ type applicationCmd struct { BasicAuth *bool `help:"Enable/Disable basic authentication for the application."` ChangeBasicAuthPassword *bool `help:"Generate a new basic auth password."` Env map[string]string `help:"Environment variables which are passed to the app at runtime."` + SensitiveEnv map[string]string `help:"Sensitive environment variables which are passed to the app at runtime."` DeleteEnv *[]string `help:"Runtime environment variables names which are to be deleted."` - BuildEnv map[string]string `help:"Environment variables names which are passed to the app build process."` - DeleteBuildEnv *[]string `help:"Build environment variables which are to be deleted."` + + BuildEnv map[string]string `help:"Environment variables names which are passed to the app build process."` + SensitiveBuildEnv map[string]string `help:"Sensitive environment variables names which are passed to the app build process."` + DeleteBuildEnv *[]string `help:"Build environment variables which are to be deleted."` + // DeployJob, ScheduledJob and WorkerJob are embedded pointers to // structs. Due to the usage of kong these pointers will never be `nil`. // So checking for `nil` values can not be used to find out if some of @@ -263,32 +267,53 @@ func (cmd *applicationCmd) applyUpdates(app *apps.Application) { app.Spec.ForProvider.Language = apps.Language(*cmd.Language) } - runtimeEnv := make(map[string]string) - if cmd.Env != nil { - runtimeEnv = cmd.Env + runtimeEnv := cmd.Env + if runtimeEnv == nil { + runtimeEnv = make(map[string]string) + } + + sensitiveRuntimeEnv := cmd.SensitiveEnv + if sensitiveRuntimeEnv == nil { + sensitiveRuntimeEnv = make(map[string]string) } + if cmd.RetryRelease != nil && *cmd.RetryRelease { runtimeEnv[ReleaseTrigger] = triggerTimestamp() } + var delEnv []string if cmd.DeleteEnv != nil { delEnv = *cmd.DeleteEnv } - app.Spec.ForProvider.Config.Env = util.UpdateEnvVars(app.Spec.ForProvider.Config.Env, runtimeEnv, delEnv) - buildEnv := make(map[string]string) - if cmd.BuildEnv != nil { - buildEnv = cmd.BuildEnv + app.Spec.ForProvider.Config.Env = util.UpdateEnvVars(app.Spec.ForProvider.Config.Env, runtimeEnv, sensitiveRuntimeEnv, delEnv) + + // build env vars + buildEnv := cmd.BuildEnv + if buildEnv == nil { + buildEnv = make(map[string]string) + } + + sensitiveBuildEnv := cmd.SensitiveBuildEnv + if sensitiveBuildEnv == nil { + sensitiveBuildEnv = make(map[string]string) } + if cmd.RetryBuild != nil && *cmd.RetryBuild { buildEnv[BuildTrigger] = triggerTimestamp() } - var buildDelEnv []string + + var delBuildEnv []string if cmd.DeleteBuildEnv != nil { - buildDelEnv = *cmd.DeleteBuildEnv + delBuildEnv = *cmd.DeleteBuildEnv } - app.Spec.ForProvider.BuildEnv = util.UpdateEnvVars(app.Spec.ForProvider.BuildEnv, buildEnv, buildDelEnv) + app.Spec.ForProvider.BuildEnv = util.UpdateEnvVars( + app.Spec.ForProvider.BuildEnv, + buildEnv, + sensitiveBuildEnv, + delBuildEnv, + ) if cmd.Pause != nil && *cmd.Pause { app.Spec.ForProvider.Paused = *cmd.Pause } diff --git a/update/application_test.go b/update/application_test.go index 42346e19..bbbbd220 100644 --- a/update/application_test.go +++ b/update/application_test.go @@ -51,7 +51,7 @@ func TestApplication(t *testing.T) { Size: initialSize, Replicas: ptr.To(int32(1)), Port: ptr.To(int32(1337)), - Env: util.EnvVarsFromMap(map[string]string{"foo": "bar"}), + Env: util.EnvVarsFromMap(map[string]string{"foo": "bar", "poo": "blue"}), EnableBasicAuth: ptr.To(false), DeployJob: &apps.DeployJob{ Job: apps.Job{ @@ -114,13 +114,14 @@ func TestApplication(t *testing.T) { SubPath: ptr.To("new/path"), Revision: ptr.To("some-change"), }, - Size: ptr.To("newsize"), - Port: ptr.To(int32(1234)), - Replicas: ptr.To(int32(999)), - Hosts: &[]string{"one.example.org", "two.example.org"}, - Env: map[string]string{"bar": "zoo"}, - BuildEnv: map[string]string{"BP_GO_TARGETS": "./cmd/web-server"}, - BasicAuth: ptr.To(true), + Size: ptr.To("newsize"), + Port: ptr.To(int32(1234)), + Replicas: ptr.To(int32(999)), + Hosts: &[]string{"one.example.org", "two.example.org"}, + Env: map[string]string{"bar": "zoo"}, + SensitiveEnv: map[string]string{"secret": "orange"}, + BuildEnv: map[string]string{"BP_GO_TARGETS": "./cmd/web-server"}, + BasicAuth: ptr.To(true), DeployJob: &deployJob{ Command: ptr.To("exit 0"), Name: ptr.To("exit"), Retries: ptr.To(int32(1)), Timeout: ptr.To(time.Minute * 5), @@ -136,8 +137,15 @@ func TestApplication(t *testing.T) { assert.Equal(t, *cmd.Replicas, *updated.Spec.ForProvider.Config.Replicas) assert.Equal(t, *cmd.BasicAuth, *updated.Spec.ForProvider.Config.EnableBasicAuth) assert.Equal(t, *cmd.Hosts, updated.Spec.ForProvider.Hosts) - assert.Equal(t, util.UpdateEnvVars(existingApp.Spec.ForProvider.Config.Env, cmd.Env, nil), updated.Spec.ForProvider.Config.Env) - assert.Equal(t, util.UpdateEnvVars(existingApp.Spec.ForProvider.BuildEnv, cmd.BuildEnv, nil), updated.Spec.ForProvider.BuildEnv) + assert.Equal(t, util.UpdateEnvVars(existingApp.Spec.ForProvider.Config.Env, cmd.Env, cmd.SensitiveEnv, nil), updated.Spec.ForProvider.Config.Env) + assert.Equal(t, util.UpdateEnvVars(existingApp.Spec.ForProvider.BuildEnv, cmd.BuildEnv, cmd.SensitiveBuildEnv, nil), updated.Spec.ForProvider.BuildEnv) + + secretKeyEnv := util.EnvVarByName(updated.Spec.ForProvider.Config.Env, "secret") + require.NotNil(t, secretKeyEnv, "secret environment variable should exist") + require.NotNil(t, secretKeyEnv.Sensitive) + assert.Equal(t, "orange", secretKeyEnv.Value) + assert.True(t, *secretKeyEnv.Sensitive) + assert.Equal(t, *cmd.DeployJob.Command, updated.Spec.ForProvider.Config.DeployJob.Command) assert.Equal(t, *cmd.DeployJob.Name, updated.Spec.ForProvider.Config.DeployJob.Name) assert.Equal(t, *cmd.DeployJob.Timeout, updated.Spec.ForProvider.Config.DeployJob.Timeout.Duration) @@ -156,7 +164,13 @@ func TestApplication(t *testing.T) { DeleteEnv: &[]string{"foo"}, }, checkApp: func(t *testing.T, cmd applicationCmd, orig, updated *apps.Application) { - assert.Empty(t, updated.Spec.ForProvider.Config.Env) + foundFoo := false + for _, env := range updated.Spec.ForProvider.Config.Env { + if env.Name == "foo" { + foundFoo = true + } + } + assert.False(t, foundFoo) assert.NotEmpty(t, updated.Spec.ForProvider.BuildEnv) }, }, @@ -187,6 +201,22 @@ func TestApplication(t *testing.T) { assert.NotEmpty(t, updated.Spec.ForProvider.Config.Env) }, }, + "update variable from normal/sensitive": { + orig: existingApp, + cmd: applicationCmd{ + resourceCmd: resourceCmd{ + Name: existingApp.Name, + }, + SensitiveEnv: map[string]string{"poo": "blue"}, + }, + checkApp: func(t *testing.T, cmd applicationCmd, orig, updated *apps.Application) { + sensitivePoo := util.EnvVarByName(updated.Spec.ForProvider.Config.Env, "poo") + require.NotNil(t, sensitivePoo) + require.NotNil(t, sensitivePoo.Sensitive) + assert.True(t, *sensitivePoo.Sensitive) + }, + }, + "change basic auth password": { orig: existingApp, cmd: applicationCmd{