Skip to content

Commit a5492a4

Browse files
committed
feat: Add sensitive environment variable to nctl applications
This change adds new flags (--sensitive-env and --sensitive-build-env) to the application create and update commands. These flags let users add to environment variables sensitive=true. masking logic was added too to make their values hidden from the command output. Test cases were added to confirm that sensitive environment variables are correctly handled.
1 parent 253f629 commit a5492a4

File tree

6 files changed

+144
-32
lines changed

6 files changed

+144
-32
lines changed

api/util/apps.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,28 @@ func VerifiedAppHosts(app *apps.Application) []string {
6969
return verifiedHosts
7070
}
7171

72-
func EnvVarsFromMap(env map[string]string) apps.EnvVars {
72+
type EnvVarModifier func(envVar *apps.EnvVar)
73+
74+
func EnvVarsFromMap(env map[string]string, options ...EnvVarModifier) apps.EnvVars {
7375
vars := apps.EnvVars{}
7476
for k, v := range env {
75-
vars = append(vars, apps.EnvVar{Name: k, Value: v})
77+
envVar := apps.EnvVar{Name: k, Value: v}
78+
for _, opt := range options {
79+
opt(&envVar)
80+
}
81+
vars = append(vars, envVar)
7682
}
7783
return vars
7884
}
7985

80-
func UpdateEnvVars(oldEnvs []apps.EnvVar, newEnvs map[string]string, toDelete []string) apps.EnvVars {
81-
if len(newEnvs) == 0 && len(toDelete) == 0 {
86+
func Sensitive() EnvVarModifier {
87+
return func(envVar *apps.EnvVar) {
88+
envVar.Sensitive = ptr.To(true)
89+
}
90+
}
91+
92+
func UpdateEnvVars(oldEnvs []apps.EnvVar, newEnvs, sensitiveEnvs map[string]string, toDelete []string) apps.EnvVars {
93+
if len(newEnvs) == 0 && len(sensitiveEnvs) == 0 && len(toDelete) == 0 {
8294
return oldEnvs
8395
}
8496

@@ -91,6 +103,10 @@ func UpdateEnvVars(oldEnvs []apps.EnvVar, newEnvs map[string]string, toDelete []
91103
for _, v := range new {
92104
envMap[v.Name] = v
93105
}
106+
sensitive := EnvVarsFromMap(sensitiveEnvs, Sensitive())
107+
for _, v := range sensitive {
108+
envMap[v.Name] = v
109+
}
94110

95111
for _, v := range toDelete {
96112
delete(envMap, v)
@@ -119,7 +135,11 @@ func EnvVarToString(envs apps.EnvVars) string {
119135

120136
var keyValuePairs []string
121137
for _, env := range envs {
122-
keyValuePairs = append(keyValuePairs, fmt.Sprintf("%v=%v", env.Name, env.Value))
138+
if env.Sensitive != nil && *env.Sensitive {
139+
keyValuePairs = append(keyValuePairs, fmt.Sprintf("%v=*****", env.Name))
140+
} else {
141+
keyValuePairs = append(keyValuePairs, fmt.Sprintf("%v=%v", env.Name, env.Value))
142+
}
123143
}
124144

125145
return strings.Join(keyValuePairs, ";")

api/util/apps_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func TestEnvUpdate(t *testing.T) {
2525
}
2626
up := map[string]string{"old2": "val2"}
2727
del := []string{"old3"}
28-
new := UpdateEnvVars(old, up, del)
28+
new := UpdateEnvVars(old, up, nil, del)
2929
expected := apps.EnvVars{
3030
{
3131
Name: "old1",

create/application.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"os"
8+
"slices"
89
"strconv"
910
"strings"
1011
"time"
@@ -42,7 +43,9 @@ type applicationCmd struct {
4243
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."`
4344
BasicAuth *bool `help:"Enable/Disable basic authentication for the app (defaults to ${app_default_basic_auth})." placeholder:"${app_default_basic_auth}"`
4445
Env map[string]string `help:"Environment variables which are passed to the app at runtime."`
46+
SensitiveEnv map[string]string `help:"Sensitive environment variables which are passed to the app at runtime."`
4547
BuildEnv map[string]string `help:"Environment variables which are passed to the app build process."`
48+
SensitiveBuildEnv map[string]string `help:"Sensitive environment variables which are passed to the app build process."`
4649
DeployJob deployJob `embed:"" prefix:"deploy-job-"`
4750
WorkerJob workerJob `embed:"" prefix:"worker-job-"`
4851
ScheduledJob scheduledJob `embed:"" prefix:"scheduled-job-"`
@@ -273,6 +276,13 @@ func spinnerMessage(msg, icon string, sleepTime time.Duration) error {
273276
return spinner.Stop()
274277
}
275278

279+
func combineEnvVars(plain, sensitive map[string]string) apps.EnvVars {
280+
return slices.Concat(
281+
util.EnvVarsFromMap(plain),
282+
util.EnvVarsFromMap(sensitive, util.Sensitive()),
283+
)
284+
}
285+
276286
func (app *applicationCmd) config() apps.Config {
277287
var deployJob *apps.DeployJob
278288

@@ -288,10 +298,9 @@ func (app *applicationCmd) config() apps.Config {
288298
},
289299
}
290300
}
291-
292301
config := apps.Config{
293302
EnableBasicAuth: app.BasicAuth,
294-
Env: util.EnvVarsFromMap(app.Env),
303+
Env: combineEnvVars(app.Env, app.SensitiveEnv),
295304
DeployJob: deployJob,
296305
}
297306

@@ -358,7 +367,7 @@ func (app *applicationCmd) newApplication(project string) *apps.Application {
358367
},
359368
Hosts: app.Hosts,
360369
Config: app.config(),
361-
BuildEnv: util.EnvVarsFromMap(app.BuildEnv),
370+
BuildEnv: combineEnvVars(app.BuildEnv, app.SensitiveBuildEnv),
362371
DockerfileBuild: apps.DockerfileBuild{
363372
Enabled: app.DockerfileBuild.Enabled,
364373
DockerfilePath: app.DockerfileBuild.Path,

create/application_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,34 @@ func TestApplication(t *testing.T) {
440440
assert.Nil(t, app.Spec.ForProvider.Git.Auth)
441441
},
442442
},
443+
"with sensitive env": {
444+
cmd: applicationCmd{
445+
resourceCmd: resourceCmd{
446+
Name: "sensitive-env-test",
447+
},
448+
Git: gitConfig{
449+
URL: "https://github.com/ninech/doesnotexist.git",
450+
SubPath: "/my/app",
451+
Revision: "superbug",
452+
},
453+
SensitiveEnv: map[string]string{"secret": "orange"},
454+
SensitiveBuildEnv: map[string]string{"build_secret": "banana"},
455+
SkipRepoAccessCheck: true,
456+
},
457+
checkApp: func(t *testing.T, cmd applicationCmd, app *apps.Application) {
458+
env := util.EnvVarByName(app.Spec.ForProvider.Config.Env, "secret")
459+
require.NotNil(t, env)
460+
require.NotNil(t, env.Sensitive)
461+
assert.True(t, *env.Sensitive)
462+
assert.Equal(t, "orange", env.Value)
463+
464+
buildEnv := util.EnvVarByName(app.Spec.ForProvider.BuildEnv, "build_secret")
465+
require.NotNil(t, buildEnv)
466+
require.NotNil(t, buildEnv.Sensitive)
467+
assert.True(t, *buildEnv.Sensitive)
468+
assert.Equal(t, "banana", buildEnv.Value)
469+
},
470+
},
443471
}
444472

445473
for name, tc := range cases {

update/application.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,13 @@ type applicationCmd struct {
3535
BasicAuth *bool `help:"Enable/Disable basic authentication for the application."`
3636
ChangeBasicAuthPassword *bool `help:"Generate a new basic auth password."`
3737
Env map[string]string `help:"Environment variables which are passed to the app at runtime."`
38+
SensitiveEnv map[string]string `help:"Sensitive environment variables which are passed to the app at runtime."`
3839
DeleteEnv *[]string `help:"Runtime environment variables names which are to be deleted."`
39-
BuildEnv map[string]string `help:"Environment variables names which are passed to the app build process."`
40-
DeleteBuildEnv *[]string `help:"Build environment variables which are to be deleted."`
40+
41+
BuildEnv map[string]string `help:"Environment variables names which are passed to the app build process."`
42+
SensitiveBuildEnv map[string]string `help:"Sensitive environment variables names which are passed to the app build process."`
43+
DeleteBuildEnv *[]string `help:"Build environment variables which are to be deleted."`
44+
4145
// DeployJob, ScheduledJob and WorkerJob are embedded pointers to
4246
// structs. Due to the usage of kong these pointers will never be `nil`.
4347
// 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) {
263267
app.Spec.ForProvider.Language = apps.Language(*cmd.Language)
264268
}
265269

266-
runtimeEnv := make(map[string]string)
267-
if cmd.Env != nil {
268-
runtimeEnv = cmd.Env
270+
runtimeEnv := cmd.Env
271+
if runtimeEnv == nil {
272+
runtimeEnv = make(map[string]string)
273+
}
274+
275+
sensitiveRuntimeEnv := cmd.SensitiveEnv
276+
if sensitiveRuntimeEnv == nil {
277+
sensitiveRuntimeEnv = make(map[string]string)
269278
}
279+
270280
if cmd.RetryRelease != nil && *cmd.RetryRelease {
271281
runtimeEnv[ReleaseTrigger] = triggerTimestamp()
272282
}
283+
273284
var delEnv []string
274285
if cmd.DeleteEnv != nil {
275286
delEnv = *cmd.DeleteEnv
276287
}
277-
app.Spec.ForProvider.Config.Env = util.UpdateEnvVars(app.Spec.ForProvider.Config.Env, runtimeEnv, delEnv)
278288

279-
buildEnv := make(map[string]string)
280-
if cmd.BuildEnv != nil {
281-
buildEnv = cmd.BuildEnv
289+
app.Spec.ForProvider.Config.Env = util.UpdateEnvVars(app.Spec.ForProvider.Config.Env, runtimeEnv, sensitiveRuntimeEnv, delEnv)
290+
291+
// build env vars
292+
buildEnv := cmd.BuildEnv
293+
if buildEnv == nil {
294+
buildEnv = make(map[string]string)
295+
}
296+
297+
sensitiveBuildEnv := cmd.SensitiveBuildEnv
298+
if sensitiveBuildEnv == nil {
299+
sensitiveBuildEnv = make(map[string]string)
282300
}
301+
283302
if cmd.RetryBuild != nil && *cmd.RetryBuild {
284303
buildEnv[BuildTrigger] = triggerTimestamp()
285304
}
286-
var buildDelEnv []string
305+
306+
var delBuildEnv []string
287307
if cmd.DeleteBuildEnv != nil {
288-
buildDelEnv = *cmd.DeleteBuildEnv
308+
delBuildEnv = *cmd.DeleteBuildEnv
289309
}
290-
app.Spec.ForProvider.BuildEnv = util.UpdateEnvVars(app.Spec.ForProvider.BuildEnv, buildEnv, buildDelEnv)
291310

311+
app.Spec.ForProvider.BuildEnv = util.UpdateEnvVars(
312+
app.Spec.ForProvider.BuildEnv,
313+
buildEnv,
314+
sensitiveBuildEnv,
315+
delBuildEnv,
316+
)
292317
if cmd.Pause != nil && *cmd.Pause {
293318
app.Spec.ForProvider.Paused = *cmd.Pause
294319
}

update/application_test.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestApplication(t *testing.T) {
5151
Size: initialSize,
5252
Replicas: ptr.To(int32(1)),
5353
Port: ptr.To(int32(1337)),
54-
Env: util.EnvVarsFromMap(map[string]string{"foo": "bar"}),
54+
Env: util.EnvVarsFromMap(map[string]string{"foo": "bar", "poo": "blue"}),
5555
EnableBasicAuth: ptr.To(false),
5656
DeployJob: &apps.DeployJob{
5757
Job: apps.Job{
@@ -114,13 +114,14 @@ func TestApplication(t *testing.T) {
114114
SubPath: ptr.To("new/path"),
115115
Revision: ptr.To("some-change"),
116116
},
117-
Size: ptr.To("newsize"),
118-
Port: ptr.To(int32(1234)),
119-
Replicas: ptr.To(int32(999)),
120-
Hosts: &[]string{"one.example.org", "two.example.org"},
121-
Env: map[string]string{"bar": "zoo"},
122-
BuildEnv: map[string]string{"BP_GO_TARGETS": "./cmd/web-server"},
123-
BasicAuth: ptr.To(true),
117+
Size: ptr.To("newsize"),
118+
Port: ptr.To(int32(1234)),
119+
Replicas: ptr.To(int32(999)),
120+
Hosts: &[]string{"one.example.org", "two.example.org"},
121+
Env: map[string]string{"bar": "zoo"},
122+
SensitiveEnv: map[string]string{"secret": "orange"},
123+
BuildEnv: map[string]string{"BP_GO_TARGETS": "./cmd/web-server"},
124+
BasicAuth: ptr.To(true),
124125
DeployJob: &deployJob{
125126
Command: ptr.To("exit 0"), Name: ptr.To("exit"),
126127
Retries: ptr.To(int32(1)), Timeout: ptr.To(time.Minute * 5),
@@ -136,8 +137,15 @@ func TestApplication(t *testing.T) {
136137
assert.Equal(t, *cmd.Replicas, *updated.Spec.ForProvider.Config.Replicas)
137138
assert.Equal(t, *cmd.BasicAuth, *updated.Spec.ForProvider.Config.EnableBasicAuth)
138139
assert.Equal(t, *cmd.Hosts, updated.Spec.ForProvider.Hosts)
139-
assert.Equal(t, util.UpdateEnvVars(existingApp.Spec.ForProvider.Config.Env, cmd.Env, nil), updated.Spec.ForProvider.Config.Env)
140-
assert.Equal(t, util.UpdateEnvVars(existingApp.Spec.ForProvider.BuildEnv, cmd.BuildEnv, nil), updated.Spec.ForProvider.BuildEnv)
140+
assert.Equal(t, util.UpdateEnvVars(existingApp.Spec.ForProvider.Config.Env, cmd.Env, cmd.SensitiveEnv, nil), updated.Spec.ForProvider.Config.Env)
141+
assert.Equal(t, util.UpdateEnvVars(existingApp.Spec.ForProvider.BuildEnv, cmd.BuildEnv, cmd.SensitiveBuildEnv, nil), updated.Spec.ForProvider.BuildEnv)
142+
143+
secretKeyEnv := util.EnvVarByName(updated.Spec.ForProvider.Config.Env, "secret")
144+
require.NotNil(t, secretKeyEnv, "secret environment variable should exist")
145+
require.NotNil(t, secretKeyEnv.Sensitive)
146+
assert.Equal(t, "orange", secretKeyEnv.Value)
147+
assert.True(t, *secretKeyEnv.Sensitive)
148+
141149
assert.Equal(t, *cmd.DeployJob.Command, updated.Spec.ForProvider.Config.DeployJob.Command)
142150
assert.Equal(t, *cmd.DeployJob.Name, updated.Spec.ForProvider.Config.DeployJob.Name)
143151
assert.Equal(t, *cmd.DeployJob.Timeout, updated.Spec.ForProvider.Config.DeployJob.Timeout.Duration)
@@ -156,7 +164,13 @@ func TestApplication(t *testing.T) {
156164
DeleteEnv: &[]string{"foo"},
157165
},
158166
checkApp: func(t *testing.T, cmd applicationCmd, orig, updated *apps.Application) {
159-
assert.Empty(t, updated.Spec.ForProvider.Config.Env)
167+
foundFoo := false
168+
for _, env := range updated.Spec.ForProvider.Config.Env {
169+
if env.Name == "foo" {
170+
foundFoo = true
171+
}
172+
}
173+
assert.False(t, foundFoo)
160174
assert.NotEmpty(t, updated.Spec.ForProvider.BuildEnv)
161175
},
162176
},
@@ -187,6 +201,22 @@ func TestApplication(t *testing.T) {
187201
assert.NotEmpty(t, updated.Spec.ForProvider.Config.Env)
188202
},
189203
},
204+
"update variable from normal/sensitive": {
205+
orig: existingApp,
206+
cmd: applicationCmd{
207+
resourceCmd: resourceCmd{
208+
Name: existingApp.Name,
209+
},
210+
SensitiveEnv: map[string]string{"poo": "blue"},
211+
},
212+
checkApp: func(t *testing.T, cmd applicationCmd, orig, updated *apps.Application) {
213+
sensitivePoo := util.EnvVarByName(updated.Spec.ForProvider.Config.Env, "poo")
214+
require.NotNil(t, sensitivePoo)
215+
require.NotNil(t, sensitivePoo.Sensitive)
216+
assert.True(t, *sensitivePoo.Sensitive)
217+
},
218+
},
219+
190220
"change basic auth password": {
191221
orig: existingApp,
192222
cmd: applicationCmd{

0 commit comments

Comments
 (0)