Skip to content

Commit 90e121b

Browse files
committed
feat: fix review findings
1 parent 0da36dd commit 90e121b

File tree

3 files changed

+93
-38
lines changed

3 files changed

+93
-38
lines changed

internal/cmd/beta/image/create/create.go

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,14 @@ func NewCmd(p *print.Printer) *cobra.Command {
8585
Long: "Creates images.",
8686
Args: args.NoArgs,
8787
Example: examples.Build(
88-
examples.NewExample(`Create a named imaged`, `$ stackit beta image create --name my-new-image --disk-format=raw --local-file-path=/my/raw/image`),
89-
examples.NewExample(`Create a named image with labels`, `$ stackit beta image create --name my-new-image --disk-format=raw --local-file-path=/my/raw/image--labels dev,amd64`),
88+
examples.NewExample(
89+
`Create a named image 'my-new-image' from a raw disk image located in '/my/raw/image'`,
90+
`$ stackit beta image create --name my-new-image --disk-format=raw --local-file-path=/my/raw/image`,
91+
),
92+
examples.NewExample(
93+
`Create a named image 'my-new-image' from a qcow2 image read from '/my/qcow2/image' with labels describing its contents`,
94+
`$ stackit beta image create --name my-new-image --disk-format=qcow2 --local-file-path=/my/qcow2/image--labels os=linux,distro=alpine,version=3.12`,
95+
),
9096
),
9197
RunE: func(cmd *cobra.Command, _ []string) (err error) {
9298
ctx := context.Background()
@@ -132,7 +138,7 @@ func NewCmd(p *print.Printer) *cobra.Command {
132138
if !ok {
133139
return fmt.Errorf("create image: no upload URL has been provided")
134140
}
135-
if err := uploadFile(ctx, p, file, *url); err != nil {
141+
if err := uploadAsync(ctx, p, file, *url); err != nil {
136142
return err
137143
}
138144

@@ -148,41 +154,69 @@ func NewCmd(p *print.Printer) *cobra.Command {
148154
return cmd
149155
}
150156

151-
func uploadFile(ctx context.Context, p *print.Printer, file *os.File, url string) (err error) {
152-
var filesize int64
153-
if stat, err := file.Stat(); err != nil {
154-
p.Debug(print.DebugLevel, "create image: cannot open file %q: %w", file.Name(), err)
155-
} else {
156-
filesize = stat.Size()
157-
}
158-
p.Debug(print.DebugLevel, "uploading image to %s", url)
157+
func uploadAsync(ctx context.Context, p *print.Printer, file *os.File, url string) error {
158+
ticker := time.NewTicker(5 * time.Second)
159+
ch := uploadFile(ctx, p, file, url)
160+
159161
start := time.Now()
160-
// pass the file contents as stream, as they can get arbitrarily large. We do
161-
// _not_ want to load them into an internal buffer. The downside is, that we
162-
// have to set the content-length header manually
163-
uploadRequest, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bufio.NewReader(file))
164-
if err != nil {
165-
return fmt.Errorf("create image: cannot create request: %w", err)
162+
for {
163+
select {
164+
case <-ticker.C:
165+
p.Info("uploading for %s\n", time.Since(start))
166+
case err := <-ch:
167+
return err
168+
}
166169
}
167-
uploadRequest.Header.Add("Content-Type", "application/octet-stream")
168-
uploadRequest.ContentLength = filesize
170+
}
169171

170-
uploadResponse, err := http.DefaultClient.Do(uploadRequest)
171-
if err != nil {
172-
return fmt.Errorf("create image: error contacting server for upload: %w", err)
173-
}
174-
defer func() {
175-
if inner := uploadResponse.Body.Close(); inner != nil {
176-
err = fmt.Errorf("error closing file: %wqq (%w)", inner, err)
172+
func uploadFile(ctx context.Context, p *print.Printer, file *os.File, url string) chan error {
173+
ch := make(chan error)
174+
go func() {
175+
defer close(ch)
176+
var filesize int64
177+
if stat, err := file.Stat(); err != nil {
178+
ch <- fmt.Errorf("create image: cannot read file size %q: %w", file.Name(), err)
179+
return
180+
} else {
181+
filesize = stat.Size()
182+
}
183+
p.Debug(print.DebugLevel, "uploading image to %s", url)
184+
185+
start := time.Now()
186+
// pass the file contents as stream, as they can get arbitrarily large. We do
187+
// _not_ want to load them into an internal buffer. The downside is, that we
188+
// have to set the content-length header manually
189+
uploadRequest, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bufio.NewReader(file))
190+
if err != nil {
191+
ch <- fmt.Errorf("create image: cannot create request: %w", err)
192+
return
177193
}
194+
uploadRequest.Header.Add("Content-Type", "application/octet-stream")
195+
uploadRequest.ContentLength = filesize
196+
197+
uploadResponse, err := http.DefaultClient.Do(uploadRequest)
198+
if err != nil {
199+
ch <- fmt.Errorf("create image: error contacting server for upload: %w", err)
200+
return
201+
}
202+
defer func() {
203+
if inner := uploadResponse.Body.Close(); inner != nil {
204+
err = fmt.Errorf("error closing file: %w (%w)", inner, err)
205+
}
206+
}()
207+
if uploadResponse.StatusCode != http.StatusOK {
208+
ch <- fmt.Errorf("create image: server rejected image upload with %s", uploadResponse.Status)
209+
return
210+
}
211+
delay := time.Since(start)
212+
p.Debug(print.DebugLevel, "uploaded %d bytes in %v", filesize, delay)
213+
214+
ch <- nil
215+
return
216+
178217
}()
179-
if uploadResponse.StatusCode != http.StatusOK {
180-
return fmt.Errorf("create image: server rejected image upload with %s", uploadResponse.Status)
181-
}
182-
delay := time.Since(start)
183-
p.Debug(print.DebugLevel, "uploaded %d bytes in %v", filesize, delay)
184218

185-
return nil
219+
return ch
186220
}
187221

188222
func configureFlags(cmd *cobra.Command) {

internal/cmd/beta/image/list/list.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import (
2323
type inputModel struct {
2424
*globalflags.GlobalFlagModel
2525
LabelSelector *string
26+
Limit *int64
2627
}
2728

2829
const (
2930
labelSelectorFlag = "label-selector"
31+
limitFlag = "limit"
3032
)
3133

3234
func NewCmd(p *print.Printer) *cobra.Command {
@@ -36,8 +38,18 @@ func NewCmd(p *print.Printer) *cobra.Command {
3638
Long: "Lists images by their internal ID.",
3739
Args: args.NoArgs,
3840
Example: examples.Build(
39-
examples.NewExample(`List all images`, `$ stackit beta image list`),
40-
examples.NewExample(`List images with label`, `$ stackit beta image list --label-selector ARM64,dev`),
41+
examples.NewExample(
42+
`List all images`,
43+
`$ stackit beta image list`,
44+
),
45+
examples.NewExample(
46+
`List images with label`,
47+
`$ stackit beta image list --label-selector ARM64,dev`,
48+
),
49+
examples.NewExample(
50+
`List the first 10 images`,
51+
`$ stackit beta image list --limit=10`,
52+
),
4153
),
4254
RunE: func(cmd *cobra.Command, _ []string) error {
4355
ctx := context.Background()
@@ -69,6 +81,9 @@ func NewCmd(p *print.Printer) *cobra.Command {
6981
if items := response.GetItems(); items == nil || len(*items) == 0 {
7082
p.Info("No images found for project %q", projectLabel)
7183
} else {
84+
if model.Limit != nil && len(*items) > int(*model.Limit) {
85+
*items = (*items)[:*model.Limit]
86+
}
7287
if err := outputResult(p, model.OutputFormat, *items); err != nil {
7388
return fmt.Errorf("output images: %w", err)
7489
}
@@ -84,6 +99,7 @@ func NewCmd(p *print.Printer) *cobra.Command {
8499

85100
func configureFlags(cmd *cobra.Command) {
86101
cmd.Flags().String(labelSelectorFlag, "", "Filter by label")
102+
cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements")
87103
}
88104

89105
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
@@ -95,6 +111,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
95111
model := inputModel{
96112
GlobalFlagModel: globalFlags,
97113
LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag),
114+
Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag),
98115
}
99116

100117
if p.IsVerbosityDebug() {

internal/cmd/beta/image/list/list_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package list
22

33
import (
44
"context"
5+
"strconv"
56
"testing"
67

78
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
@@ -19,16 +20,18 @@ var projectIdFlag = globalflags.ProjectIdFlag
1920
type testCtxKey struct{}
2021

2122
var (
22-
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
23-
testClient = &iaas.APIClient{}
24-
testProjectId = uuid.NewString()
25-
testLabels = "fooKey=fooValue,barKey=barValue,bazKey=bazValue"
23+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
24+
testClient = &iaas.APIClient{}
25+
testProjectId = uuid.NewString()
26+
testLabels = "fooKey=fooValue,barKey=barValue,bazKey=bazValue"
27+
testLimit int64 = 10
2628
)
2729

2830
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
2931
flagValues := map[string]string{
3032
projectIdFlag: testProjectId,
3133
labelSelectorFlag: testLabels,
34+
limitFlag: strconv.Itoa(int(testLimit)),
3235
}
3336
for _, mod := range mods {
3437
mod(flagValues)
@@ -40,6 +43,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
4043
model := &inputModel{
4144
GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault},
4245
LabelSelector: utils.Ptr(testLabels),
46+
Limit: &testLimit,
4347
}
4448
for _, mod := range mods {
4549
mod(model)

0 commit comments

Comments
 (0)