Skip to content

Commit 08cb29b

Browse files
feat(stack): starter-kit bucket and user (#553)
1 parent 89e4b92 commit 08cb29b

3 files changed

Lines changed: 120 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- starter-kit stack to create also bucket and user for managed object storage
13+
1014
## [3.24.0] - 2025-10-01
1115

1216
### Added

internal/commands/stack/starterkit/deploy.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ func (s *deployStarterKitCommand) deploy(exec commands.Executor) (string, error)
116116
var cluster *upcloud.KubernetesCluster
117117
var db *upcloud.ManagedDatabase
118118
var objStorage *upcloud.ManagedObjectStorage
119+
var objStorageAccessKey *upcloud.ManagedObjectStorageUserAccessKey
120+
var objStorageBucket string
119121
var kubeconfigPath string
120122

121123
// Create kubernetes cluster
@@ -138,7 +140,7 @@ func (s *deployStarterKitCommand) deploy(exec commands.Executor) (string, error)
138140

139141
// Create object storage
140142
eg.Go(func() error {
141-
objStorage, err = createObjectStorage(ctx, exec, config, network)
143+
objStorage, objStorageAccessKey, objStorageBucket, err = createObjectStorage(ctx, exec, config, network)
142144
if err != nil {
143145
return fmt.Errorf("failed to create object storage: %w", err)
144146
}
@@ -192,5 +194,5 @@ func (s *deployStarterKitCommand) deploy(exec commands.Executor) (string, error)
192194
exec.PushProgressSuccess(msg)
193195

194196
// Create summary and return
195-
return buildSummary(cluster, kubeconfigPath, network, router, db, objStorage), nil
197+
return buildSummary(cluster, kubeconfigPath, network, router, db, objStorage, objStorageAccessKey, objStorageBucket), nil
196198
}

internal/commands/stack/starterkit/starterkit.go

Lines changed: 112 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ import (
1313

1414
type Version string
1515

16-
const (
17-
VersionV1 Version = "v1.0.0"
18-
)
19-
2016
type StarterKitConfig struct {
2117
ProjectName string
2218
Zone string
@@ -133,103 +129,113 @@ func buildSummary(
133129
router *upcloud.Router,
134130
db *upcloud.ManagedDatabase,
135131
obj *upcloud.ManagedObjectStorage,
132+
objAcc *upcloud.ManagedObjectStorageUserAccessKey,
133+
objBucket string,
136134
) string {
137135
var b strings.Builder
138136

139137
// Header
140-
fmt.Fprintf(&b, "Starter Kit deployed successfully!\n\n")
138+
b.WriteString("Starter Kit deployed successfully!\n\n")
141139

142140
// Kubernetes
143-
fmt.Fprintf(&b, "KUBERNETES CLUSTER\n")
141+
b.WriteString("KUBERNETES CLUSTER\n")
144142
if cluster != nil {
145-
fmt.Fprintf(&b, " Name: %s\n", cluster.Name)
146-
fmt.Fprintf(&b, " UUID: %s\n", cluster.UUID)
147-
fmt.Fprintf(&b, " Zone: %s\n", cluster.Zone)
148-
fmt.Fprintf(&b, " Network: %s\n", cluster.Network)
143+
b.WriteString(fmt.Sprintf(" Name: %s\n", cluster.Name))
144+
b.WriteString(fmt.Sprintf(" UUID: %s\n", cluster.UUID))
145+
b.WriteString(fmt.Sprintf(" Zone: %s\n", cluster.Zone))
146+
b.WriteString(fmt.Sprintf(" Network: %s\n", cluster.Network))
149147
if kubeconfigPath != "" {
150-
fmt.Fprintf(&b, " Kubeconfig: %s\n", kubeconfigPath)
151-
fmt.Fprintf(&b, " Set env: export KUBECONFIG=%s\n", kubeconfigPath)
152-
fmt.Fprintf(&b, " Test: kubectl get nodes\n")
153-
fmt.Fprintf(&b, " Ingress LB: kubectl -n ingress-nginx get svc ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}{\"\\n\"}'\n")
148+
b.WriteString(fmt.Sprintf(" Kubeconfig: %s\n", kubeconfigPath))
149+
b.WriteString(fmt.Sprintf(" Set env: export KUBECONFIG=%s\n", kubeconfigPath))
150+
b.WriteString(" Test: kubectl get nodes\n")
151+
b.WriteString(" Ingress LB: kubectl -n ingress-nginx get svc ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}{\"\\n\"}'\n")
154152
}
155153
}
156154
b.WriteString("\n")
157155

158156
// Network & Router
159-
fmt.Fprintf(&b, "NETWORKING\n")
157+
b.WriteString("NETWORKING\n")
160158
if network != nil {
161-
fmt.Fprintf(&b, " Network: %s (UUID: %s)\n", network.Name, network.UUID)
159+
b.WriteString(fmt.Sprintf(" Network: %s (UUID: %s)\n", network.Name, network.UUID))
162160
if len(network.IPNetworks) > 0 {
163-
fmt.Fprintf(&b, " CIDR: %s\n", network.IPNetworks[0].Address)
161+
b.WriteString(fmt.Sprintf(" CIDR: %s\n", network.IPNetworks[0].Address))
164162
if network.IPNetworks[0].DHCP == upcloud.True {
165-
fmt.Fprintf(&b, " DHCP: enabled\n")
163+
b.WriteString(" DHCP: enabled\n")
166164
} else {
167-
fmt.Fprintf(&b, " DHCP: disabled\n")
165+
b.WriteString(" DHCP: disabled\n")
168166
}
169167
}
170168
}
171169
if router != nil {
172-
fmt.Fprintf(&b, " Router: %s (UUID: %s)\n", router.Name, router.UUID)
170+
b.WriteString(fmt.Sprintf(" Router: %s (UUID: %s)\n", router.Name, router.UUID))
173171
}
174172
b.WriteString("\n")
175173

176174
// Managed Database
177-
fmt.Fprintf(&b, "MANAGED DATABASE\n")
175+
b.WriteString("MANAGED DATABASE\n")
178176
if db != nil {
179-
fmt.Fprintf(&b, " Name: %s (UUID: %s)\n", db.Title, db.UUID)
180-
fmt.Fprintf(&b, " Type/Plan: %s / %s\n", db.Type, db.Plan)
181-
fmt.Fprintf(&b, " State: %s\n", db.State)
182-
// TODO: We are showing the password in clear text here. Is that ok?
183-
fmt.Fprintf(&b, " ServiceURI : %s\n", db.ServiceURI)
177+
b.WriteString(fmt.Sprintf(" Name: %s (UUID: %s)\n", db.Title, db.UUID))
178+
b.WriteString(fmt.Sprintf(" Type/Plan: %s / %s\n", db.Type, db.Plan))
179+
b.WriteString(fmt.Sprintf(" State: %s\n", db.State))
180+
b.WriteString(fmt.Sprintf(" ServiceURI: %s\n", db.ServiceURI))
184181
} else {
185-
fmt.Fprintf(&b, " (not created)\n")
182+
b.WriteString(" (not created)\n")
186183
}
187184
b.WriteString("\n")
188185

189186
// Managed Object Storage
190-
fmt.Fprintf(&b, "OBJECT STORAGE\n")
187+
b.WriteString("OBJECT STORAGE\n")
191188
if obj != nil {
192-
fmt.Fprintf(&b, " Name: %s (UUID: %s)\n", obj.Name, obj.UUID)
193-
fmt.Fprintf(&b, " Region: %s\n", obj.Region)
194-
fmt.Fprintf(&b, " State: %s\n", obj.OperationalState)
189+
b.WriteString(fmt.Sprintf(" Name: %s (UUID: %s)\n", obj.Name, obj.UUID))
190+
b.WriteString(fmt.Sprintf(" Region: %s\n", obj.Region))
191+
b.WriteString(fmt.Sprintf(" State: %s\n", obj.OperationalState))
195192

196193
// If API provides endpoint(s)
197194
if len(obj.Endpoints) > 0 {
198-
fmt.Fprintf(&b, " DomainName: %s\n", obj.Endpoints[0].DomainName)
199-
fmt.Fprintf(&b, " Type: %s\n", obj.Endpoints[0].Type)
200-
fmt.Fprintf(&b, " IAMURL: %s\n", obj.Endpoints[0].IAMURL)
201-
fmt.Fprintf(&b, " STSURL: %s\n", obj.Endpoints[0].STSURL)
195+
b.WriteString(fmt.Sprintf(" DomainName: %s\n", obj.Endpoints[0].DomainName))
196+
b.WriteString(fmt.Sprintf(" Type: %s\n", obj.Endpoints[0].Type))
197+
b.WriteString(fmt.Sprintf(" IAMURL: %s\n", obj.Endpoints[0].IAMURL))
198+
b.WriteString(fmt.Sprintf(" STSURL: %s\n", obj.Endpoints[0].STSURL))
199+
}
200+
// If bucket was created
201+
if objBucket != "" {
202+
b.WriteString(fmt.Sprintf(" Bucket: %s\n", objBucket))
203+
}
204+
// If access key was created
205+
if objAcc != nil {
206+
b.WriteString(fmt.Sprintf(" AccessKey: %s\n", objAcc.AccessKeyID))
207+
b.WriteString(fmt.Sprintf(" SecretKey: %s\n", *objAcc.SecretAccessKey))
202208
}
203209
} else {
204-
fmt.Fprintf(&b, " (not created)\n")
210+
b.WriteString(" (not created)\n")
205211
}
206212
b.WriteString("\n")
207213

208214
// ACCESS without any Load Balancer
209-
fmt.Fprintf(&b, "ACCESS (no load balancer created)\n")
210-
fmt.Fprintf(&b, " 1) Local dev via port-forward (recommended for quick testing):\n")
211-
fmt.Fprintf(&b, " kubectl -n <namespace> port-forward svc/<your-service> 8080:80\n")
212-
fmt.Fprintf(&b, " # then open http://localhost:8080\n\n")
213-
214-
fmt.Fprintf(&b, " 2) NodePort (reachable inside the private network):\n")
215-
fmt.Fprintf(&b, " # switch your Service to NodePort\n")
216-
fmt.Fprintf(&b, " kubectl -n <namespace> patch svc <your-service> -p '{\"spec\":{\"type\":\"NodePort\"}}'\n")
217-
fmt.Fprintf(&b, " # find the assigned nodePort and a node's private IP\n")
218-
fmt.Fprintf(&b, " kubectl -n <namespace> get svc <your-service> -o jsonpath='{.spec.ports[0].nodePort}{\"\\n\"}'\n")
219-
fmt.Fprintf(&b, " kubectl get nodes -o wide\n")
220-
fmt.Fprintf(&b, " # then browse: http://<node-private-ip>:<nodePort>\n")
221-
fmt.Fprintf(&b, " (for external access: use a VPN/bastion into this private network)\n\n")
222-
223-
fmt.Fprintf(&b, " 3) Private-only options:\n")
224-
fmt.Fprintf(&b, " - VPN/peering to the VPC and use ClusterIP/NodePort directly\n")
225-
fmt.Fprintf(&b, " - Bastion host with SSH tunnels (e.g., ssh -L 8080:<node-ip>:<nodePort> ...)\n\n")
215+
b.WriteString("ACCESS (no load balancer created)\n")
216+
b.WriteString(" 1) Local dev via port-forward (recommended for quick testing):\n")
217+
b.WriteString(" kubectl -n <namespace> port-forward svc/<your-service> 8080:80\n")
218+
b.WriteString(" # then open http://localhost:8080\n\n")
219+
220+
b.WriteString(" 2) NodePort (reachable inside the private network):\n")
221+
b.WriteString(" # switch your Service to NodePort\n")
222+
b.WriteString(" kubectl -n <namespace> patch svc <your-service> -p '{\"spec\":{\"type\":\"NodePort\"}}'\n")
223+
b.WriteString(" # find the assigned nodePort and a node's private IP\n")
224+
b.WriteString(" kubectl -n <namespace> get svc <your-service> -o jsonpath='{.spec.ports[0].nodePort}{\"\\n\"}'\n")
225+
b.WriteString(" kubectl get nodes -o wide\n")
226+
b.WriteString(" # then browse: http://<node-private-ip>:<nodePort>\n")
227+
b.WriteString(" (for external access: use a VPN/bastion into this private network)\n\n")
228+
229+
b.WriteString(" 3) Private-only options:\n")
230+
b.WriteString(" - VPN/peering to the VPC and use ClusterIP/NodePort directly\n")
231+
b.WriteString(" - Bastion host with SSH tunnels (e.g., ssh -L 8080:<node-ip>:<nodePort> ...)\n\n")
226232

227233
// Final tips
228-
fmt.Fprintf(&b, "NEXT STEPS\n")
234+
b.WriteString("NEXT STEPS\n")
229235
if kubeconfigPath != "" {
230-
fmt.Fprintf(&b, " export KUBECONFIG=%s\n", kubeconfigPath)
236+
b.WriteString(fmt.Sprintf(" export KUBECONFIG=%s\n", kubeconfigPath))
231237
}
232-
fmt.Fprintf(&b, " Deploy ingress-nginx and your app, then point DNS (CNAME) to the LB hostname shown above.\n")
238+
b.WriteString(" Deploy ingress-nginx and your app, then point DNS (CNAME) to the LB hostname shown above.\n")
233239

234240
return b.String()
235241
}
@@ -298,11 +304,11 @@ func createDatabase(ctx context.Context, exec commands.Executor, config *Starter
298304
return db, nil
299305
}
300306

301-
func createObjectStorage(ctx context.Context, exec commands.Executor, config *StarterKitConfig, network *upcloud.Network) (*upcloud.ManagedObjectStorage, error) {
307+
func createObjectStorage(ctx context.Context, exec commands.Executor, config *StarterKitConfig, network *upcloud.Network) (*upcloud.ManagedObjectStorage, *upcloud.ManagedObjectStorageUserAccessKey, string, error) {
302308
exec.PushProgressStarted("Deploying Object Storage")
303309
region, err := stack.GetObjectStorageRegionFromZone(exec, config.Zone)
304310
if err != nil {
305-
return nil, fmt.Errorf("failed to validate object storage region from zone %q: %w, contact support because you might be using a new zone not supported by the deploy command", config.Zone, err)
311+
return nil, nil, "", fmt.Errorf("failed to validate object storage region from zone %q: %w, contact support because you might be using a new zone not supported by the deploy command", config.Zone, err)
306312
}
307313

308314
objStorage, err := exec.All().CreateManagedObjectStorage(ctx, &request.CreateManagedObjectStorageRequest{
@@ -325,8 +331,54 @@ func createObjectStorage(ctx context.Context, exec commands.Executor, config *St
325331
},
326332
})
327333
if err != nil {
328-
return nil, fmt.Errorf("failed to create object storage: %w", err)
334+
return nil, nil, "", fmt.Errorf("failed to create object storage: %w", err)
329335
}
330336
exec.PushProgressSuccess("Deploying Object Storage")
331-
return objStorage, nil
337+
338+
objStorage, err = exec.All().WaitForManagedObjectStorageOperationalState(exec.Context(), &request.WaitForManagedObjectStorageOperationalStateRequest{
339+
DesiredState: upcloud.ManagedObjectStorageOperationalStateRunning,
340+
UUID: objStorage.UUID,
341+
})
342+
if err != nil {
343+
return nil, nil, "", fmt.Errorf("error while waiting for the object storage to become online: %w", err)
344+
}
345+
346+
// Create user for the object storage
347+
user, err := exec.All().CreateManagedObjectStorageUser(exec.Context(), &request.CreateManagedObjectStorageUserRequest{
348+
Username: "starter-kit-user",
349+
ServiceUUID: objStorage.UUID,
350+
})
351+
if err != nil {
352+
return nil, nil, "", fmt.Errorf("failed to create user for the object storage: %w", err)
353+
}
354+
355+
err = exec.All().AttachManagedObjectStorageUserPolicy(exec.Context(), &request.AttachManagedObjectStorageUserPolicyRequest{
356+
ServiceUUID: objStorage.UUID,
357+
Username: user.Username,
358+
Name: "ECSS3FullAccess",
359+
})
360+
if err != nil {
361+
return nil, nil, "", fmt.Errorf("failed to attach user policy to object storage: %w", err)
362+
}
363+
364+
// Create an access key for the object storage
365+
userAccessKey, err := exec.All().CreateManagedObjectStorageUserAccessKey(exec.Context(), &request.CreateManagedObjectStorageUserAccessKeyRequest{
366+
Username: user.Username,
367+
ServiceUUID: objStorage.UUID,
368+
})
369+
if err != nil {
370+
return nil, nil, "", fmt.Errorf("failed to create access key for the object storage: %w", err)
371+
}
372+
373+
// Create a bucket for the object storage
374+
bucketName := "starter-kit-storage"
375+
_, err = exec.All().CreateManagedObjectStorageBucket(exec.Context(), &request.CreateManagedObjectStorageBucketRequest{
376+
Name: bucketName,
377+
ServiceUUID: objStorage.UUID,
378+
})
379+
if err != nil {
380+
return nil, nil, "", fmt.Errorf("failed to create bucket for the object storage: %w", err)
381+
}
382+
383+
return objStorage, userAccessKey, bucketName, nil
332384
}

0 commit comments

Comments
 (0)