Skip to content

Commit 5add2a4

Browse files
authored
feat: shared bucket support (#92)
* refactor: make template wrapper generic and move into its own package * feat: add shared bucket support * fix: replace slashes with double hyphen in volume names * refactor: create explicit shared bucket spec type
1 parent 74eeb5c commit 5add2a4

File tree

8 files changed

+400
-70
lines changed

8 files changed

+400
-70
lines changed

cmd/folder-precreator/go.mod

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module github.com/statisticsnorway/ssbucketeer-folder-precreator
2+
3+
go 1.23.1
4+
5+
require (
6+
cloud.google.com/go/storage v1.48.0
7+
golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e
8+
google.golang.org/api v0.211.0
9+
)
10+
11+
require (
12+
cel.dev/expr v0.16.1 // indirect
13+
cloud.google.com/go v0.116.0 // indirect
14+
cloud.google.com/go/auth v0.12.1 // indirect
15+
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
16+
cloud.google.com/go/compute/metadata v0.5.2 // indirect
17+
cloud.google.com/go/iam v1.2.2 // indirect
18+
cloud.google.com/go/monitoring v1.21.2 // indirect
19+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
20+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
21+
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
22+
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
23+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
24+
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
25+
github.com/envoyproxy/go-control-plane v0.13.0 // indirect
26+
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
27+
github.com/felixge/httpsnoop v1.0.4 // indirect
28+
github.com/go-logr/logr v1.4.2 // indirect
29+
github.com/go-logr/stdr v1.2.2 // indirect
30+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
31+
github.com/google/s2a-go v0.1.8 // indirect
32+
github.com/google/uuid v1.6.0 // indirect
33+
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
34+
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
35+
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
36+
go.opencensus.io v0.24.0 // indirect
37+
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
38+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
39+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
40+
go.opentelemetry.io/otel v1.29.0 // indirect
41+
go.opentelemetry.io/otel/metric v1.29.0 // indirect
42+
go.opentelemetry.io/otel/sdk v1.29.0 // indirect
43+
go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect
44+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
45+
golang.org/x/crypto v0.30.0 // indirect
46+
golang.org/x/net v0.32.0 // indirect
47+
golang.org/x/oauth2 v0.24.0 // indirect
48+
golang.org/x/sync v0.10.0 // indirect
49+
golang.org/x/sys v0.28.0 // indirect
50+
golang.org/x/text v0.21.0 // indirect
51+
golang.org/x/time v0.8.0 // indirect
52+
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
53+
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
54+
google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect
55+
google.golang.org/grpc v1.67.2 // indirect
56+
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
57+
google.golang.org/protobuf v1.35.2 // indirect
58+
)

cmd/folder-precreator/go.sum

Lines changed: 208 additions & 0 deletions
Large diffs are not rendered by default.

cmd/main.go

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,15 @@ import (
5252
)
5353

5454
type config struct {
55-
DaplaGroupSaProjectId string `env:"DAPLA_GROUP_SA_PROJECT_ID,required,notEmpty"`
56-
TeamsFolderNumber string `env:"TEAMS_FOLDER_NUMBER,required,notEmpty"`
57-
Stage string `env:"STAGE,required,notEmpty"`
58-
IamProbeImage string `env:"IAM_PROBE_IMAGE"`
59-
PrecreatorImage string `env:"PRECREATOR_IMAGE"`
60-
ADCGroupEnvName string `env:"ADC_GROUP_ENV_NAME"`
61-
GroupConfigs []controller.AccessGroupConfig `env:"GROUP_CONFIG,required,notEmpty"`
62-
AuditSinks []auditSink `env:"AUDIT_SINKS"`
55+
DaplaGroupSaProjectId string `env:"DAPLA_GROUP_SA_PROJECT_ID,required,notEmpty"`
56+
TeamsFolderNumber string `env:"TEAMS_FOLDER_NUMBER,required,notEmpty"`
57+
Stage string `env:"STAGE,required,notEmpty"`
58+
IamProbeImage string `env:"IAM_PROBE_IMAGE"`
59+
PrecreatorImage string `env:"PRECREATOR_IMAGE"`
60+
ADCGroupEnvName string `env:"ADC_GROUP_ENV_NAME"`
61+
GroupConfigs []controller.AccessGroupConfig `env:"GROUP_CONFIG,required,notEmpty"`
62+
AuditSinks []auditSink `env:"AUDIT_SINKS"`
63+
SharedBucketTemplate controller.SharedBucketTemplate `env:"SHARED_BUCKET_TEMPLATE" envDefault:"ssb-{{.TeamName}}-data-delt-{{.BucketShortName}}-{{.Stage}}"`
6364
}
6465

6566
type auditSink struct {
@@ -228,17 +229,18 @@ func main() {
228229

229230
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
230231
(&controller.StatefulsetMutator{
231-
Client: mgr.GetClient(),
232-
Decoder: admission.NewDecoder(mgr.GetScheme()),
233-
Storage: storageClient,
234-
Projects: projectsClient,
235-
Folders: foldersClient,
236-
TeamsFolderNumber: cfg.TeamsFolderNumber,
237-
Stage: cfg.Stage,
238-
IamProbeImage: cfg.IamProbeImage,
239-
PrecreatorImage: cfg.PrecreatorImage,
240-
ADCGroupEnvName: cfg.ADCGroupEnvName,
241-
GroupConfigs: cfg.GroupConfigs,
232+
Client: mgr.GetClient(),
233+
Decoder: admission.NewDecoder(mgr.GetScheme()),
234+
Storage: storageClient,
235+
Projects: projectsClient,
236+
Folders: foldersClient,
237+
TeamsFolderNumber: cfg.TeamsFolderNumber,
238+
Stage: cfg.Stage,
239+
IamProbeImage: cfg.IamProbeImage,
240+
PrecreatorImage: cfg.PrecreatorImage,
241+
ADCGroupEnvName: cfg.ADCGroupEnvName,
242+
GroupConfigs: cfg.GroupConfigs,
243+
SharedBucketTemplate: cfg.SharedBucketTemplate,
242244
}).SetupWithManager(mgr)
243245

244246
if err = (&controller.ServiceAccountValidator{

internal/controller/common.go

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ package controller
33
import (
44
"fmt"
55
"strings"
6-
"text/template"
76
"time"
7+
8+
"github.com/statisticsnorway/ssbucketeer/internal/template"
89
)
910

1011
const (
1112
impersonateGroupAnnotation = "dapla.ssb.no/impersonate-group"
1213
mountBucketsAnnotation = "dapla.ssb.no/mount-buckets"
1314
mountStandardBucketsAnnotation = "dapla.ssb.no/mount-standard-buckets"
15+
mountSharedBucketsAnnotation = "dapla.ssb.no/mount-shared-buckets"
1416
serviceContainerAnnotation = "dapla.ssb.no/service-container-name"
1517
requestedServiceDurationAnnotation = "dapla.ssb.no/requested-service-duration"
1618
accessReasonAnnotation = "dapla.ssb.no/access-reason"
@@ -45,43 +47,11 @@ type Auther interface {
4547
UserIsMemberOf(username, group string) (bool, error)
4648
}
4749

48-
// projectTemplate wraps a *template.Template and implements yaml.Unmarshaler interface
49-
// so we can unmarshal a template string into a template.
50-
type projectTemplate struct {
51-
template *template.Template
52-
}
53-
54-
// Execute wraps template.Template.Execute to provide an easier-to-use interface
55-
// and only allow ProjectTemplateData to be passed as data.
56-
func (t projectTemplate) Execute(data ProjectTemplateData) (string, error) {
57-
if t.template == nil {
58-
return "", fmt.Errorf("template is nil, trying to execute for data %q", data)
59-
}
60-
sb := strings.Builder{}
61-
if err := t.template.Execute(&sb, data); err != nil {
62-
return "", err
63-
}
64-
return sb.String(), nil
65-
}
66-
67-
// UnmarshalYAML implements the yaml.Unmarshaler interface.
68-
func (t *projectTemplate) UnmarshalYAML(unmarshal func(any) error) error {
69-
var templateString string
70-
if err := unmarshal(&templateString); err != nil {
71-
return fmt.Errorf("unmarshal template string: %w", err)
72-
}
73-
t.template = template.New(templateString)
74-
if _, err := t.template.Parse(templateString); err != nil {
75-
return fmt.Errorf("parse template string: %w", err)
76-
}
77-
return nil
78-
}
79-
8050
type AccessGroupConfig struct {
81-
Name string `yaml:"name"`
82-
ProjectTemplate projectTemplate `yaml:"projectTemplate"`
83-
MaxDuration time.Duration `yaml:"maxDuration"`
84-
ReasonRequired bool `yaml:"reasonRequired"`
51+
Name string `yaml:"name"`
52+
ProjectTemplate template.AnonymousTemplate[ProjectTemplateData] `yaml:"projectTemplate"`
53+
MaxDuration time.Duration `yaml:"maxDuration"`
54+
ReasonRequired bool `yaml:"reasonRequired"`
8555
}
8656

8757
func (c AccessGroupConfig) ToTeam(group string) string {
@@ -93,6 +63,14 @@ type ProjectTemplateData struct {
9363
Stage string
9464
}
9565

66+
type SharedBucketTemplateData struct {
67+
TeamName string
68+
BucketShortName string
69+
Stage string
70+
}
71+
72+
type SharedBucketTemplate = template.AnonymousTemplate[SharedBucketTemplateData]
73+
9674
type AccessGroupConfigs []AccessGroupConfig
9775

9876
func (cs AccessGroupConfigs) GetConfig(group string) *AccessGroupConfig {

internal/controller/job_controller.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3131
"k8s.io/apimachinery/pkg/runtime"
3232
"k8s.io/apimachinery/pkg/types"
33+
"k8s.io/utils/ptr"
3334
ctrl "sigs.k8s.io/controller-runtime"
3435
"sigs.k8s.io/controller-runtime/pkg/client"
3536
klog "sigs.k8s.io/controller-runtime/pkg/log"
@@ -92,7 +93,7 @@ func (r *JobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
9293
replicas = 1
9394
}
9495

95-
sfs.Spec.Replicas = ptr[int32](int32(replicas))
96+
sfs.Spec.Replicas = ptr.To(int32(replicas))
9697
sfs.Annotations[iamProbeStatus] = iamProbeDone
9798

9899
// Find the service container, so we can add volumeMounts
@@ -132,7 +133,7 @@ func (r *JobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
132133
LocalObjectReference: corev1.LocalObjectReference{
133134
Name: refreshBucketsConfigMap.Name,
134135
},
135-
DefaultMode: ptr[int32](0o555), // Read, execute
136+
DefaultMode: ptr.To[int32](0o555), // Read, execute
136137
},
137138
},
138139
}

internal/controller/statefulset_webhook.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import (
1212
rm "cloud.google.com/go/resourcemanager/apiv3"
1313
rmpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
1414
"cloud.google.com/go/storage"
15+
"github.com/statisticsnorway/ssbucketeer/internal/template"
1516
"google.golang.org/api/iterator"
1617
appsv1 "k8s.io/api/apps/v1"
1718
batchv1 "k8s.io/api/batch/v1"
1819
corev1 "k8s.io/api/core/v1"
1920
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2021
"k8s.io/apimachinery/pkg/types"
22+
"k8s.io/utils/ptr"
2123
ctrl "sigs.k8s.io/controller-runtime"
2224
"sigs.k8s.io/controller-runtime/pkg/client"
2325
klog "sigs.k8s.io/controller-runtime/pkg/log"
@@ -50,6 +52,13 @@ type StatefulsetMutator struct {
5052
ADCGroupEnvName string
5153

5254
GroupConfigs AccessGroupConfigs
55+
56+
SharedBucketTemplate template.AnonymousTemplate[SharedBucketTemplateData]
57+
}
58+
59+
type SharedBucketSpec struct {
60+
Team string `json:"team"`
61+
ShortName string `json:"sharedBucket"`
5362
}
5463

5564
func (m *StatefulsetMutator) SetupWithManager(mgr ctrl.Manager) {
@@ -120,6 +129,12 @@ func (m *StatefulsetMutator) Handle(ctx context.Context, req admission.Request)
120129
}
121130
}
122131

132+
if sharedBuckets, ok := sfs.Annotations[mountSharedBucketsAnnotation]; ok {
133+
if err := m.addSharedBuckets(bucketMounts, sharedBuckets); err != nil {
134+
log.Error(err, "failed to add shared buckets")
135+
}
136+
}
137+
123138
// TODO: Use Dapla Team API for this?
124139
if sfs.Annotations[mountStandardBucketsAnnotation] == "true" {
125140
if err := m.addStandardBuckets(ctx, team, *groupConfig, bucketMounts); err != nil {
@@ -167,7 +182,7 @@ func getServiceContainer(pod *corev1.PodTemplateSpec, name string) *corev1.Conta
167182

168183
func (m *StatefulsetMutator) launchIamProbe(ctx context.Context, sfs *appsv1.StatefulSet) error {
169184
sfs.Annotations[iamProbeStatus] = fmt.Sprintf("%s%d", iamProbeRunningPrefix, *sfs.Spec.Replicas)
170-
sfs.Spec.Replicas = ptr[int32](0)
185+
sfs.Spec.Replicas = ptr.To[int32](0)
171186

172187
probeJob := &batchv1.Job{
173188
ObjectMeta: metav1.ObjectMeta{
@@ -178,8 +193,8 @@ func (m *StatefulsetMutator) launchIamProbe(ctx context.Context, sfs *appsv1.Sta
178193
},
179194
},
180195
Spec: batchv1.JobSpec{
181-
ActiveDeadlineSeconds: ptr[int64](300),
182-
TTLSecondsAfterFinished: ptr[int32](0),
196+
ActiveDeadlineSeconds: ptr.To[int64](300),
197+
TTLSecondsAfterFinished: ptr.To[int32](0),
183198
Template: corev1.PodTemplateSpec{
184199
ObjectMeta: metav1.ObjectMeta{
185200
Annotations: map[string]string{
@@ -257,7 +272,7 @@ func (m *StatefulsetMutator) addStandardBuckets(ctx context.Context, team string
257272

258273
projectName, err := gc.ProjectTemplate.Execute(ProjectTemplateData{TeamName: team, Stage: m.Stage})
259274
if err != nil {
260-
return fmt.Errorf("execute template %s: %w", gc.ProjectTemplate.template.Name(), err)
275+
return fmt.Errorf("execute template %s: %w", gc.ProjectTemplate.Name(), err)
261276
}
262277

263278
projectIt := m.Projects.SearchProjects(ctx, &rmpb.SearchProjectsRequest{
@@ -291,6 +306,28 @@ func (m *StatefulsetMutator) addStandardBuckets(ctx context.Context, team string
291306
return nil
292307
}
293308

309+
func (m *StatefulsetMutator) addSharedBuckets(bucketMounts map[string]string, sharedBuckets string) error {
310+
bucketSpecs := []SharedBucketSpec{}
311+
312+
if err := json.Unmarshal([]byte(sharedBuckets), &bucketSpecs); err != nil {
313+
return err
314+
}
315+
316+
for _, bucket := range bucketSpecs {
317+
mountPoint := fmt.Sprintf("shared/%s/%s", bucket.Team, bucket.ShortName)
318+
bucket, err := m.SharedBucketTemplate.Execute(SharedBucketTemplateData{
319+
TeamName: bucket.Team,
320+
BucketShortName: bucket.ShortName,
321+
Stage: m.Stage,
322+
})
323+
if err != nil {
324+
return err
325+
}
326+
bucketMounts[mountPoint] = bucket
327+
}
328+
return nil
329+
}
330+
294331
func ensurePodAnnotations(podTemplate *corev1.PodTemplateSpec) {
295332
if podTemplate.Annotations == nil {
296333
podTemplate.Annotations = make(map[string]string, 2)

internal/controller/volumes.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@ package controller
33
import (
44
"fmt"
55
"slices"
6+
"strings"
67

78
corev1 "k8s.io/api/core/v1"
9+
"k8s.io/utils/ptr"
810
)
911

10-
// ptr is a convenience function for generating pointers of primitive types
11-
func ptr[T any](t T) *T {
12-
return &t
13-
}
14-
1512
func volumeNameIs(name string) func(v corev1.Volume) bool {
1613
return func(v corev1.Volume) bool {
1714
return v.Name == name
@@ -28,7 +25,7 @@ func addBucketsToPodSpec(podspec *corev1.PodSpec, container *corev1.Container, b
2825
volumes := make([]corev1.Volume, 0, len(bucketMounts))
2926
volumeMounts := make([]corev1.VolumeMount, 0, len(bucketMounts))
3027
for mountPoint, bucket := range bucketMounts {
31-
volumeName := fmt.Sprintf("gcsfuse-%s", mountPoint)
28+
volumeName := fmt.Sprintf("gcsfuse-%s", strings.ReplaceAll(mountPoint, "/", "--"))
3229

3330
// TODO: Test whether the group has read/write access
3431
if !slices.ContainsFunc(podspec.Volumes, volumeNameIs(volumeName)) {
@@ -37,7 +34,7 @@ func addBucketsToPodSpec(podspec *corev1.PodSpec, container *corev1.Container, b
3734
VolumeSource: corev1.VolumeSource{
3835
CSI: &corev1.CSIVolumeSource{
3936
Driver: "gcsfuse.csi.storage.gke.io",
40-
ReadOnly: ptr(false),
37+
ReadOnly: ptr.To(false),
4138
VolumeAttributes: map[string]string{
4239
"bucketName": bucket,
4340
"mountOptions": "uid=1000,gid=100",
@@ -72,7 +69,7 @@ func addBucketsToPodSpec(podspec *corev1.PodSpec, container *corev1.Container, b
7269
Name: "bucket-folders-precreator",
7370
}
7471
for mountPoint, bucket := range bucketMounts {
75-
volumeName := fmt.Sprintf("gcsfuse-%s", mountPoint)
72+
volumeName := fmt.Sprintf("gcsfuse-%s", strings.ReplaceAll(mountPoint, "/", "--"))
7673
precreator.VolumeMounts = append(precreator.VolumeMounts, corev1.VolumeMount{
7774
Name: volumeName,
7875
MountPath: fmt.Sprintf("/buckets/%s", bucket),

0 commit comments

Comments
 (0)