@@ -13,10 +13,6 @@ import (
1313
1414type Version string
1515
16- const (
17- VersionV1 Version = "v1.0.0"
18- )
19-
2016type 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