Skip to content

Commit d4080c2

Browse files
committed
feat: added update command
1 parent 291db82 commit d4080c2

File tree

3 files changed

+669
-1
lines changed

3 files changed

+669
-1
lines changed

internal/cmd/beta/image/image.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
package security_group
1+
package image
22

33
import (
44
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/create"
55
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/delete"
66
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/describe"
77
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/list"
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/image/update"
89
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
910
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
1011

@@ -31,5 +32,6 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) {
3132
list.NewCmd(p),
3233
delete.NewCmd(p),
3334
describe.NewCmd(p),
35+
update.NewCmd(p),
3436
)
3537
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package update
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
16+
iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
18+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
19+
)
20+
21+
type imageConfig struct {
22+
BootMenu *bool
23+
CdromBus *string
24+
DiskBus *string
25+
NicModel *string
26+
OperatingSystem *string
27+
OperatingSystemDistro *string
28+
OperatingSystemVersion *string
29+
RescueBus *string
30+
RescueDevice *string
31+
SecureBoot *bool
32+
Uefi *bool
33+
VideoModel *string
34+
VirtioScsi *bool
35+
}
36+
37+
func (ic *imageConfig) isEmpty() bool {
38+
return ic.BootMenu == nil &&
39+
ic.CdromBus == nil &&
40+
ic.DiskBus == nil &&
41+
ic.NicModel == nil &&
42+
ic.OperatingSystem == nil &&
43+
ic.OperatingSystemDistro == nil &&
44+
ic.OperatingSystemVersion == nil &&
45+
ic.RescueBus == nil &&
46+
ic.RescueDevice == nil &&
47+
ic.SecureBoot == nil &&
48+
ic.Uefi == nil &&
49+
ic.VideoModel == nil &&
50+
ic.VirtioScsi == nil
51+
}
52+
53+
type inputModel struct {
54+
*globalflags.GlobalFlagModel
55+
56+
Id string
57+
Name *string
58+
DiskFormat *string
59+
LocalFilePath *string
60+
Labels *map[string]string
61+
Config *imageConfig
62+
MinDiskSize *int64
63+
MinRam *int64
64+
Protected *bool
65+
}
66+
67+
func (im *inputModel) isEmpty() bool {
68+
return im.Name == nil &&
69+
im.DiskFormat == nil &&
70+
im.LocalFilePath == nil &&
71+
im.Labels == nil &&
72+
(im.Config == nil || im.Config.isEmpty()) &&
73+
im.MinDiskSize == nil &&
74+
im.MinRam == nil &&
75+
im.Protected == nil
76+
}
77+
78+
const imageIdArg = "IMAGE_ID"
79+
80+
const (
81+
nameFlag = "name"
82+
diskFormatFlag = "disk-format"
83+
localFilePathFlag = "local-file-path"
84+
85+
bootMenuFlag = "boot-menu"
86+
cdromBusFlag = "cdrom-bus"
87+
diskBusFlag = "disk-bus"
88+
nicModelFlag = "nic-model"
89+
operatingSystemFlag = "os"
90+
operatingSystemDistroFlag = "os-distro"
91+
operatingSystemVersionFlag = "os-version"
92+
rescueBusFlag = "rescue-bus"
93+
rescueDeviceFlag = "rescue-device"
94+
secureBootFlag = "secure-boot"
95+
uefiFlag = "uefi"
96+
videoModelFlag = "video-model"
97+
virtioScsiFlag = "virtio-scsi"
98+
99+
labelsFlag = "labels"
100+
101+
minDiskSizeFlag = "min-disk-size"
102+
minRamFlag = "min-ram"
103+
ownerFlag = "owner"
104+
protectedFlag = "protected"
105+
)
106+
107+
func NewCmd(p *print.Printer) *cobra.Command {
108+
cmd := &cobra.Command{
109+
Use: fmt.Sprintf("update %s", imageIdArg),
110+
Short: "Updates an image",
111+
Long: "Updates an image",
112+
Args: args.SingleArg(imageIdArg, utils.ValidateUUID),
113+
Example: examples.Build(
114+
examples.NewExample(`Update the name of image "xxx"`, `$ stackit beta image update xxx --name my-new-name`),
115+
examples.NewExample(`Update the labels of image "xxx"`, `$ stackit beta image update xxx --labels label1=value1,label2=value2`),
116+
),
117+
RunE: func(cmd *cobra.Command, args []string) error {
118+
ctx := context.Background()
119+
model, err := parseInput(p, cmd, args)
120+
if err != nil {
121+
return err
122+
}
123+
124+
// Configure API client
125+
apiClient, err := client.ConfigureClient(p)
126+
if err != nil {
127+
return err
128+
}
129+
130+
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
131+
if err != nil {
132+
p.Debug(print.ErrorLevel, "get project name: %v", err)
133+
projectLabel = model.ProjectId
134+
}
135+
136+
imageLabel, err := iaasUtils.GetImageName(ctx, apiClient, model.ProjectId, model.Id)
137+
if err != nil {
138+
p.Warn("cannot retrieve image name: %v", err)
139+
imageLabel = model.Id
140+
}
141+
142+
if !model.AssumeYes {
143+
prompt := fmt.Sprintf("Are you sure you want to update the image %q?", imageLabel)
144+
err = p.PromptForConfirmation(prompt)
145+
if err != nil {
146+
return err
147+
}
148+
}
149+
150+
// Call API
151+
req := buildRequest(ctx, model, apiClient)
152+
153+
resp, err := req.Execute()
154+
if err != nil {
155+
return fmt.Errorf("update image: %w", err)
156+
}
157+
p.Info("Updated image \"%v\" for %q\n", utils.PtrString(resp.Name), projectLabel)
158+
159+
return nil
160+
},
161+
}
162+
163+
configureFlags(cmd)
164+
return cmd
165+
}
166+
167+
func configureFlags(cmd *cobra.Command) {
168+
cmd.Flags().String(nameFlag, "", "The name of the image.")
169+
cmd.Flags().String(diskFormatFlag, "", "The disk format of the image. ")
170+
cmd.Flags().String(localFilePathFlag, "", "The path to the local disk image file.")
171+
172+
cmd.Flags().Bool(bootMenuFlag, false, "Enables the BIOS bootmenu.")
173+
cmd.Flags().String(cdromBusFlag, "", "Sets CDROM bus controller type.")
174+
cmd.Flags().String(diskBusFlag, "", "Sets Disk bus controller type.")
175+
cmd.Flags().String(nicModelFlag, "", "Sets virtual nic model.")
176+
cmd.Flags().String(operatingSystemFlag, "", "Enables OS specific optimizations.")
177+
cmd.Flags().String(operatingSystemDistroFlag, "", "Operating System Distribution.")
178+
cmd.Flags().String(operatingSystemVersionFlag, "", "Version of the OS.")
179+
cmd.Flags().String(rescueBusFlag, "", "Sets the device bus when the image is used as a rescue image.")
180+
cmd.Flags().String(rescueDeviceFlag, "", "Sets the device when the image is used as a rescue image.")
181+
cmd.Flags().Bool(secureBootFlag, false, "Enables Secure Boot.")
182+
cmd.Flags().Bool(uefiFlag, false, "Enables UEFI boot.")
183+
cmd.Flags().String(videoModelFlag, "", "Sets Graphic device model.")
184+
cmd.Flags().Bool(virtioScsiFlag, false, "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.")
185+
186+
cmd.Flags().StringToString(labelsFlag, nil, "Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...'")
187+
188+
cmd.Flags().Int64(minDiskSizeFlag, 0, "Size in Gigabyte.")
189+
cmd.Flags().Int64(minRamFlag, 0, "Size in Megabyte.")
190+
cmd.Flags().Bool(protectedFlag, false, "Protected VM.")
191+
}
192+
193+
func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputModel, error) {
194+
globalFlags := globalflags.Parse(p, cmd)
195+
if globalFlags.ProjectId == "" {
196+
return nil, &errors.ProjectIdError{}
197+
}
198+
199+
model := inputModel{
200+
GlobalFlagModel: globalFlags,
201+
Id: cliArgs[0],
202+
Name: flags.FlagToStringPointer(p, cmd, nameFlag),
203+
204+
DiskFormat: flags.FlagToStringPointer(p, cmd, diskFormatFlag),
205+
LocalFilePath: flags.FlagToStringPointer(p, cmd, localFilePathFlag),
206+
Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag),
207+
Config: &imageConfig{
208+
BootMenu: flags.FlagToBoolPointer(p, cmd, bootMenuFlag),
209+
CdromBus: flags.FlagToStringPointer(p, cmd, cdromBusFlag),
210+
DiskBus: flags.FlagToStringPointer(p, cmd, diskBusFlag),
211+
NicModel: flags.FlagToStringPointer(p, cmd, nicModelFlag),
212+
OperatingSystem: flags.FlagToStringPointer(p, cmd, operatingSystemFlag),
213+
OperatingSystemDistro: flags.FlagToStringPointer(p, cmd, operatingSystemDistroFlag),
214+
OperatingSystemVersion: flags.FlagToStringPointer(p, cmd, operatingSystemVersionFlag),
215+
RescueBus: flags.FlagToStringPointer(p, cmd, rescueBusFlag),
216+
RescueDevice: flags.FlagToStringPointer(p, cmd, rescueDeviceFlag),
217+
SecureBoot: flags.FlagToBoolPointer(p, cmd, secureBootFlag),
218+
Uefi: flags.FlagToBoolPointer(p, cmd, uefiFlag),
219+
VideoModel: flags.FlagToStringPointer(p, cmd, videoModelFlag),
220+
VirtioScsi: flags.FlagToBoolPointer(p, cmd, virtioScsiFlag),
221+
},
222+
MinDiskSize: flags.FlagToInt64Pointer(p, cmd, minDiskSizeFlag),
223+
MinRam: flags.FlagToInt64Pointer(p, cmd, minRamFlag),
224+
Protected: flags.FlagToBoolPointer(p, cmd, protectedFlag),
225+
}
226+
227+
if model.isEmpty() {
228+
return nil, fmt.Errorf("no flags have been passed")
229+
}
230+
231+
if p.IsVerbosityDebug() {
232+
modelStr, err := print.BuildDebugStrFromInputModel(model)
233+
if err != nil {
234+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
235+
} else {
236+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
237+
}
238+
}
239+
240+
return &model, nil
241+
}
242+
243+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateImageRequest {
244+
request := apiClient.UpdateImage(ctx, model.ProjectId, model.Id)
245+
payload := iaas.NewUpdateImagePayload()
246+
var labelsMap *map[string]any
247+
if model.Labels != nil && len(*model.Labels) > 0 {
248+
// convert map[string]string to map[string]interface{}
249+
labelsMap = utils.Ptr(map[string]interface{}{})
250+
for k, v := range *model.Labels {
251+
(*labelsMap)[k] = v
252+
}
253+
}
254+
// Config *ImageConfig `json:"config,omitempty"`
255+
payload.DiskFormat = model.DiskFormat
256+
payload.Labels = labelsMap
257+
payload.MinDiskSize = model.MinDiskSize
258+
payload.MinRam = model.MinRam
259+
payload.Name = model.Name
260+
payload.Protected = model.Protected
261+
262+
if model.Config != nil {
263+
payload.Config = &iaas.ImageConfig{
264+
BootMenu: model.Config.BootMenu,
265+
CdromBus: iaas.NewNullableString(model.Config.CdromBus),
266+
DiskBus: iaas.NewNullableString(model.Config.DiskBus),
267+
NicModel: iaas.NewNullableString(model.Config.NicModel),
268+
OperatingSystem: model.Config.OperatingSystem,
269+
OperatingSystemDistro: iaas.NewNullableString(model.Config.OperatingSystemDistro),
270+
OperatingSystemVersion: iaas.NewNullableString(model.Config.OperatingSystemVersion),
271+
RescueBus: iaas.NewNullableString(model.Config.RescueBus),
272+
RescueDevice: iaas.NewNullableString(model.Config.RescueDevice),
273+
SecureBoot: model.Config.SecureBoot,
274+
Uefi: model.Config.Uefi,
275+
VideoModel: iaas.NewNullableString(model.Config.VideoModel),
276+
VirtioScsi: model.Config.VirtioScsi,
277+
}
278+
}
279+
280+
request = request.UpdateImagePayload(*payload)
281+
282+
return request
283+
}

0 commit comments

Comments
 (0)