diff --git a/cmd/compute/disk_size.go b/cmd/compute/disk_size.go new file mode 100644 index 000000000..18df8528b --- /dev/null +++ b/cmd/compute/disk_size.go @@ -0,0 +1,23 @@ +package compute + +import "fmt" + +const gibibyte int64 = 1 << 30 + +// ResolveTemplateDiskSize returns a disk size that can contain the selected template. +func ResolveTemplateDiskSize(diskSize, templateSizeBytes int64, explicitlySet bool) (int64, error) { + minimumDiskSize := (templateSizeBytes + gibibyte - 1) / gibibyte + if diskSize >= minimumDiskSize { + return diskSize, nil + } + if explicitlySet { + return 0, fmt.Errorf( + "--disk-size %d GiB is smaller than the selected template's minimum of %d GiB; use --disk-size %d or larger", + diskSize, + minimumDiskSize, + minimumDiskSize, + ) + } + + return minimumDiskSize, nil +} diff --git a/cmd/compute/disk_size_test.go b/cmd/compute/disk_size_test.go new file mode 100644 index 000000000..60122010d --- /dev/null +++ b/cmd/compute/disk_size_test.go @@ -0,0 +1,58 @@ +package compute + +import "testing" + +func TestResolveTemplateDiskSize(t *testing.T) { + tests := []struct { + name string + diskSize int64 + templateSize int64 + explicitlySet bool + want int64 + wantErr string + }{ + { + name: "default is large enough", + diskSize: 50, + templateSize: 10 * gibibyte, + want: 50, + }, + { + name: "default is increased", + diskSize: 50, + templateSize: 80 * gibibyte, + want: 80, + }, + { + name: "template size is rounded up", + diskSize: 50, + templateSize: 80*gibibyte + 1, + want: 81, + }, + { + name: "explicit size is rejected", + diskSize: 50, + templateSize: 80 * gibibyte, + explicitlySet: true, + wantErr: "--disk-size 50 GiB is smaller than the selected template's minimum of 80 GiB; use --disk-size 80 or larger", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ResolveTemplateDiskSize(tt.diskSize, tt.templateSize, tt.explicitlySet) + if tt.wantErr != "" { + if err == nil || err.Error() != tt.wantErr { + t.Fatalf("ResolveTemplateDiskSize() error = %v, want %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("ResolveTemplateDiskSize() error = %v", err) + } + if got != tt.want { + t.Errorf("ResolveTemplateDiskSize() = %d, want %d", got, tt.want) + } + }) + } +} diff --git a/cmd/compute/instance/instance_create.go b/cmd/compute/instance/instance_create.go index c9d85ab6d..d3422a5a8 100644 --- a/cmd/compute/instance/instance_create.go +++ b/cmd/compute/instance/instance_create.go @@ -16,6 +16,7 @@ import ( "golang.org/x/crypto/ssh" exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/cmd/compute" "github.com/exoscale/cli/pkg/account" "github.com/exoscale/cli/pkg/globalstate" "github.com/exoscale/cli/pkg/output" @@ -88,6 +89,38 @@ func (c *instanceCreateCmd) CmdRun(cmd *cobra.Command, _ []string) error { //nol return err } + templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(c.TemplateVisibility))) + if err != nil { + return fmt.Errorf("error listing template with visibility %q: %w", c.TemplateVisibility, err) + } + template, err := templates.FindTemplate(c.Template) + if err != nil { + return fmt.Errorf( + "no template %q found with visibility %s in zone %s", + c.Template, + c.TemplateVisibility, + c.Zone, + ) + } + diskSize, err := compute.ResolveTemplateDiskSize( + c.DiskSize, + template.Size, + cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.DiskSize)), + ) + if err != nil { + return err + } + if diskSize != c.DiskSize { + fmt.Fprintf( + os.Stderr, + "warning: template %q requires at least %d GiB; increasing the default disk size from %d GiB to %d GiB\n", + template.Name, + diskSize, + c.DiskSize, + diskSize, + ) + } + var sshKeys []v3.SSHKey for _, sshkeyName := range c.SSHKeys { sshKeys = append(sshKeys, v3.SSHKey{Name: sshkeyName}) @@ -104,13 +137,14 @@ func (c *instanceCreateCmd) CmdRun(cmd *cobra.Command, _ []string) error { //nol } instanceReq := v3.CreateInstanceRequest{ - DiskSize: c.DiskSize, + DiskSize: diskSize, PublicIPAssignment: publicIPAssignment, TpmEnabled: &c.TPM, SecurebootEnabled: &c.SecureBoot, Labels: c.Labels, Name: c.Name, SSHKeys: sshKeys, + Template: &v3.Template{ID: template.ID}, } if l := len(c.AntiAffinityGroups); l > 0 { @@ -226,21 +260,6 @@ func (c *instanceCreateCmd) CmdRun(cmd *cobra.Command, _ []string) error { //nol instanceReq.SSHKeys = []v3.SSHKey{{Name: sshKeyName}} } - templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(c.TemplateVisibility))) - if err != nil { - return fmt.Errorf("error listing template with visibility %q: %w", c.TemplateVisibility, err) - } - template, err := templates.FindTemplate(c.Template) - if err != nil { - return fmt.Errorf( - "no template %q found with visibility %s in zone %s", - c.Template, - c.TemplateVisibility, - c.Zone, - ) - } - instanceReq.Template = &v3.Template{ID: template.ID} - if c.CloudInitFile != "" { userData, err := userdata.GetUserDataFromFile(c.CloudInitFile, c.CloudInitCompress) if err != nil { diff --git a/cmd/compute/instance_pool/instance_pool_create.go b/cmd/compute/instance_pool/instance_pool_create.go index e437bd461..94e609ebf 100644 --- a/cmd/compute/instance_pool/instance_pool_create.go +++ b/cmd/compute/instance_pool/instance_pool_create.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/cmd/compute" "github.com/exoscale/cli/pkg/account" "github.com/exoscale/cli/pkg/globalstate" "github.com/exoscale/cli/pkg/output" @@ -62,7 +63,7 @@ func (c *instancePoolCreateCmd) CmdPreRun(cmd *cobra.Command, args []string) err return exocmd.CliCommandDefaultPreRun(c, cmd, args) } -func (c *instancePoolCreateCmd) CmdRun(_ *cobra.Command, _ []string) error { +func (c *instancePoolCreateCmd) CmdRun(cmd *cobra.Command, _ []string) error { ctx := exocmd.GContext client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, c.Zone) @@ -70,11 +71,43 @@ func (c *instancePoolCreateCmd) CmdRun(_ *cobra.Command, _ []string) error { return err } + templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(c.TemplateVisibility))) + if err != nil { + return fmt.Errorf("error listing template with visibility %q: %w", c.TemplateVisibility, err) + } + template, err := templates.FindTemplate(c.Template) + if err != nil { + return fmt.Errorf( + "no template %q found with visibility %s in zone %s", + c.Template, + c.TemplateVisibility, + c.Zone, + ) + } + diskSize, err := compute.ResolveTemplateDiskSize( + c.DiskSize, + template.Size, + cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.DiskSize)), + ) + if err != nil { + return err + } + if diskSize != c.DiskSize { + fmt.Fprintf( + os.Stderr, + "warning: template %q requires at least %d GiB; increasing the default disk size from %d GiB to %d GiB\n", + template.Name, + diskSize, + c.DiskSize, + diskSize, + ) + } + sshKey := &v3.SSHKey{Name: c.SSHKey} instancePoolReq := v3.CreateInstancePoolRequest{ Description: c.Description, - DiskSize: c.DiskSize, + DiskSize: diskSize, Ipv6Enabled: &c.IPv6, InstancePrefix: c.InstancePrefix, Labels: c.Labels, @@ -82,6 +115,7 @@ func (c *instancePoolCreateCmd) CmdRun(_ *cobra.Command, _ []string) error { Name: c.Name, SSHKey: sshKey, Size: c.Size, + Template: &v3.Template{ID: template.ID}, } if l := len(c.AntiAffinityGroups); l > 0 { @@ -184,21 +218,6 @@ func (c *instancePoolCreateCmd) CmdRun(_ *cobra.Command, _ []string) error { instancePoolReq.SSHKey = &v3.SSHKey{Name: account.CurrentAccount.DefaultSSHKey} } - templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(c.TemplateVisibility))) - if err != nil { - return fmt.Errorf("error listing template with visibility %q: %w", c.TemplateVisibility, err) - } - template, err := templates.FindTemplate(c.Template) - if err != nil { - return fmt.Errorf( - "no template %q found with visibility %s in zone %s", - c.Template, - c.TemplateVisibility, - c.Zone, - ) - } - instancePoolReq.Template = &v3.Template{ID: template.ID} - if c.CloudInitFile != "" { userData, err := userdata.GetUserDataFromFile(c.CloudInitFile, c.CloudInitCompress) if err != nil {