Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 3835fb1

Browse files
authored
msp: add support for secret volume mounts (#61185)
Closes https://github.com/sourcegraph/managed-services/issues/589, which blocks sourcegraph/managed-services#655 This allows operators to specify volumes to mount from secrets. The configuration is intentionally limited for now, for example: ```yaml secretVolumes: myVolumeName: mountPath: "foobar.pem" secret: "foobar" ``` It has similar capabilities as `secretEnv`, e.g. being able to source from external secrets and automatically have the appropriate access provisioned. ## Test plan n/a - we use this already internally with Redis certs. This just exposes the same functionality to operators
1 parent d445842 commit 3835fb1

File tree

6 files changed

+148
-32
lines changed

6 files changed

+148
-32
lines changed

dev/managedservicesplatform/managedservicesplatform.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func (r *Renderer) RenderEnvironment(
108108
Image: svc.Build.Image,
109109
Service: svc.Service,
110110
SecretEnv: env.SecretEnv,
111+
SecretVolumes: env.SecretVolumes,
111112
PreventDestroys: preventDestroys,
112113

113114
IsFinalStageOfRollout: rolloutPipeline != nil,

dev/managedservicesplatform/spec/environment.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ type EnvironmentSpec struct {
7878
// target project will be automatically granted.
7979
SecretEnv map[string]string `yaml:"secretEnv,omitempty"`
8080

81+
// SecretVolumes configures volumes to mount from secrets. Keys are used
82+
// as volume names.
83+
SecretVolumes map[string]EnvironmentSecretVolume `yaml:"secretVolumes,omitempty"`
84+
8185
// Resources configures additional resources that a service may depend on.
8286
Resources *EnvironmentResourcesSpec `yaml:"resources,omitempty"`
8387

@@ -121,6 +125,12 @@ func (s EnvironmentSpec) Validate() []error {
121125
errs = append(errs, s.Deploy.Validate()...)
122126
errs = append(errs, s.Resources.Validate()...)
123127
errs = append(errs, s.Instances.Validate()...)
128+
for k, v := range s.SecretVolumes {
129+
if k == "" {
130+
errs = append(errs, errors.New("secretVolumes key cannot be empty"))
131+
}
132+
errs = append(errs, v.Validate()...)
133+
}
124134

125135
// Validate service-specific specs
126136
errs = append(errs, s.EnvironmentServiceSpec.Validate()...)
@@ -639,6 +649,30 @@ func (s *EnvironmentJobScheduleSpec) FindMaxCronInterval() (*time.Duration, erro
639649
return &maxGap, nil
640650
}
641651

652+
type EnvironmentSecretVolume struct {
653+
// MountPath is the path within the container where the secret will be
654+
// mounted. The mounted file is read-only.
655+
MountPath string `yaml:"mountPath"`
656+
// Secret is name of the secret in the service's project to populate in the
657+
// environment.
658+
//
659+
// To point to a secret in another project, use the format
660+
// 'projects/{project}/secrets/{secretName}' in the value. Access to the
661+
// target project will be automatically granted.
662+
Secret string `yaml:"secret"`
663+
}
664+
665+
func (v EnvironmentSecretVolume) Validate() []error {
666+
var errs []error
667+
if v.MountPath == "" {
668+
errs = append(errs, errors.New("mountPath is required"))
669+
}
670+
if v.Secret == "" {
671+
errs = append(errs, errors.New("secret is required"))
672+
}
673+
return errs
674+
}
675+
642676
type EnvironmentResourcesSpec struct {
643677
// Redis, if provided, provisions a Redis instance backed by Cloud Memorystore.
644678
// Details for using this Redis instance is automatically provided in

dev/managedservicesplatform/stacks/cloudrun/cloudrun.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ func NewStack(stacks *stack.Set, vars Variables) (crossStackOutput *CrossStackOu
157157
return nil, errors.Wrap(err, "add user env vars")
158158
}
159159

160+
// Add user-configured secret volumes
161+
addContainerSecretVolumes(cloudRunBuilder, vars.Environment.SecretVolumes)
162+
160163
// Load image tag from tfvars.
161164
imageTag := tfvar.New(stack, id, tfvar.Config{
162165
VariableKey: tfVarKeyResolvedImageTag,
@@ -491,6 +494,24 @@ func addContainerEnvVars(
491494
return nil
492495
}
493496

497+
func addContainerSecretVolumes(
498+
b builder.Builder,
499+
volumes map[string]spec.EnvironmentSecretVolume,
500+
) {
501+
keys := maps.Keys(volumes)
502+
slices.Sort(keys)
503+
for _, k := range keys {
504+
v := volumes[k]
505+
b.AddSecretVolume(k, v.MountPath,
506+
builder.SecretRef{
507+
Name: v.Secret,
508+
Version: "latest",
509+
},
510+
292, // 0444 read-only
511+
)
512+
}
513+
}
514+
494515
func makeContainerResourceLimits(r spec.EnvironmentInstancesResourcesSpec) map[string]*string {
495516
return map[string]*string{
496517
"cpu": pointers.Ptr(strconv.Itoa(r.CPU)),

dev/managedservicesplatform/stacks/iam/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ go_test(
3535
srcs = ["iam_test.go"],
3636
embed = [":iam"],
3737
deps = [
38+
"//dev/managedservicesplatform/spec",
3839
"//lib/pointers",
3940
"@com_github_hexops_autogold_v2//:autogold",
4041
"@com_github_stretchr_testify//assert",

dev/managedservicesplatform/stacks/iam/iam.go

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type Variables struct {
4141

4242
// SecretEnv should be the environment config that sources from secrets.
4343
SecretEnv map[string]string
44+
// SecretVolumes should be the environment config that mounts volumes from secrets.
45+
SecretVolumes map[string]spec.EnvironmentSecretVolume
4446

4547
// IsFinalStageOfRollout should be true if BuildRolloutPipelineConfiguration
4648
// provides a non-nil configuration for an environment.
@@ -202,7 +204,7 @@ func NewStack(stacks *stack.Set, vars Variables) (*CrossStackOutput, error) {
202204

203205
// If any secret env secrets are external to this project, grant access to
204206
// the referenced secrets.
205-
if externalSecrets, err := extractExternalSecrets(vars.SecretEnv); err != nil {
207+
if externalSecrets, err := extractExternalSecrets(vars.SecretEnv, vars.SecretVolumes); err != nil {
206208
return nil, errors.Wrap(err, "extracting secret projects")
207209
} else {
208210
secretAccessID := id.Group("external_secret_access")
@@ -300,44 +302,70 @@ type externalSecret struct {
300302
secretID string
301303
}
302304

303-
func extractExternalSecrets(secrets map[string]string) ([]externalSecret, error) {
305+
func extractExternalSecrets(secretEnv map[string]string, secretVolumes map[string]spec.EnvironmentSecretVolume) ([]externalSecret, error) {
304306
var externalSecrets []externalSecret
305307

306-
// Sort for stability
307-
secretKeys := maps.Keys(secrets)
308+
// Add secretEnv
309+
secretKeys := maps.Keys(secretEnv)
308310
sort.Strings(secretKeys)
309311
for _, k := range secretKeys {
310-
secretName := secrets[k]
311-
// Error on easy-to-make oopsies
312-
if strings.HasPrefix(secretName, "project/") {
313-
return nil, errors.Newf("invalid secret name %q: 'project/'-prefixed name provided, did you mean 'projects/'?",
314-
secretName)
312+
secretName := secretEnv[k]
313+
es, err := getExternalSecretFromSecretName(k, secretName)
314+
if err != nil {
315+
return nil, err
315316
}
316-
// Crude check to tell users that they shouldn't include versions in their secrets
317-
if strings.Contains(secretName, "/versions/") {
318-
return nil, errors.Newf("invalid secret name %q: secrets should not be versioned with '/version/'",
319-
secretName)
317+
if es != nil {
318+
externalSecrets = append(externalSecrets, *es)
320319
}
321-
// Check for 'projects/{project}/secrets/{secretName}'
322-
if strings.HasPrefix(secretName, "projects/") {
323-
secretNameParts := strings.SplitN(secretName, "/", 4)
324-
if len(secretNameParts) != 4 {
325-
return nil, errors.Newf("invalid secret name %q: expected 'projects/'-prefixed name to have 4 '/'-delimited parts",
326-
secretName)
327-
}
328-
// Error on easy-to-make oopsies
329-
if secretNameParts[2] != "secrets" {
330-
return nil, errors.Newf("invalid secret name %q: found '/secret/' segment, did you mean '/secrets/'?",
331-
secretName)
332-
}
333-
334-
externalSecrets = append(externalSecrets, externalSecret{
335-
key: strings.ToLower(k),
336-
projectID: secretNameParts[1],
337-
secretID: secretNameParts[3],
338-
})
320+
}
321+
322+
// Add secretVolumes
323+
secretKeys = maps.Keys(secretVolumes)
324+
sort.Strings(secretKeys)
325+
for _, k := range secretKeys {
326+
secretName := secretVolumes[k].Secret
327+
es, err := getExternalSecretFromSecretName(fmt.Sprintf("volume_%s", k), secretName)
328+
if err != nil {
329+
return nil, err
330+
}
331+
if es != nil {
332+
externalSecrets = append(externalSecrets, *es)
339333
}
340334
}
341335

342336
return externalSecrets, nil
343337
}
338+
339+
func getExternalSecretFromSecretName(key, secretName string) (*externalSecret, error) {
340+
// Error on easy-to-make oopsies
341+
if strings.HasPrefix(secretName, "project/") {
342+
return nil, errors.Newf("invalid secret name %q: 'project/'-prefixed name provided, did you mean 'projects/'?",
343+
secretName)
344+
}
345+
// Crude check to tell users that they shouldn't include versions in their secrets
346+
if strings.Contains(secretName, "/versions/") {
347+
return nil, errors.Newf("invalid secret name %q: secrets should not be versioned with '/version/'",
348+
secretName)
349+
}
350+
// Check for 'projects/{project}/secrets/{secretName}'
351+
if strings.HasPrefix(secretName, "projects/") {
352+
secretNameParts := strings.SplitN(secretName, "/", 4)
353+
if len(secretNameParts) != 4 {
354+
return nil, errors.Newf("invalid secret name %q: expected 'projects/'-prefixed name to have 4 '/'-delimited parts",
355+
secretName)
356+
}
357+
// Error on easy-to-make oopsies
358+
if secretNameParts[2] != "secrets" {
359+
return nil, errors.Newf("invalid secret name %q: found '/secret/' segment, did you mean '/secrets/'?",
360+
secretName)
361+
}
362+
363+
return &externalSecret{
364+
key: strings.ToLower(key),
365+
projectID: secretNameParts[1],
366+
secretID: secretNameParts[3],
367+
}, nil
368+
}
369+
370+
return nil, nil
371+
}

dev/managedservicesplatform/stacks/iam/iam_test.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/stretchr/testify/assert"
88
"github.com/stretchr/testify/require"
99

10+
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/spec"
1011
"github.com/sourcegraph/sourcegraph/lib/pointers"
1112
)
1213

@@ -48,6 +49,7 @@ func TestExtractExternalSecrets(t *testing.T) {
4849
for _, tc := range []struct {
4950
name string
5051
secretEnv map[string]string
52+
secretVolumes map[string]spec.EnvironmentSecretVolume
5153
wantExternalSecrets []externalSecret
5254
wantError autogold.Value
5355
}{
@@ -84,9 +86,38 @@ func TestExtractExternalSecrets(t *testing.T) {
8486
secretID: "BAR",
8587
}},
8688
},
89+
{
90+
name: "volumes has external secret",
91+
secretVolumes: map[string]spec.EnvironmentSecretVolume{
92+
"secret": {Secret: "projects/foo/secrets/VOLUME"},
93+
"secret2": {Secret: "SEKRET_VOLUME"},
94+
},
95+
wantExternalSecrets: []externalSecret{{
96+
key: "volume_secret",
97+
projectID: "foo",
98+
secretID: "VOLUME",
99+
}},
100+
},
101+
{
102+
name: "external secrets from volumes and env",
103+
secretEnv: map[string]string{"SEKRET": "projects/foo/secrets/BAR", "NOT_EXTERNAL": "SEKRET"},
104+
secretVolumes: map[string]spec.EnvironmentSecretVolume{
105+
"secret": {Secret: "projects/foo/secrets/VOLUME"},
106+
"secret2": {Secret: "SEKRET_VOLUME"},
107+
},
108+
wantExternalSecrets: []externalSecret{{
109+
key: "sekret",
110+
projectID: "foo",
111+
secretID: "BAR",
112+
}, {
113+
key: "volume_secret",
114+
projectID: "foo",
115+
secretID: "VOLUME",
116+
}},
117+
},
87118
} {
88119
t.Run(tc.name, func(t *testing.T) {
89-
got, err := extractExternalSecrets(tc.secretEnv)
120+
got, err := extractExternalSecrets(tc.secretEnv, tc.secretVolumes)
90121
if tc.wantError != nil {
91122
require.Error(t, err)
92123
tc.wantError.Equal(t, err.Error())

0 commit comments

Comments
 (0)