Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instance Create: Migrate to egoscale v3 and add multiple sshkeys #620

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Improvements

- Instance Create: Migrate to egoscale v3 and add multiple sshkeys #620

## 1.78.4

### Improvements
Expand Down
165 changes: 113 additions & 52 deletions cmd/instance_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import (
exossh "github.com/exoscale/cli/pkg/ssh"
"github.com/exoscale/cli/pkg/userdata"
"github.com/exoscale/cli/utils"
egoscale "github.com/exoscale/egoscale/v2"
exoapi "github.com/exoscale/egoscale/v2/api"
v3 "github.com/exoscale/egoscale/v3"
)

type instanceCreateCmd struct {
Expand All @@ -41,11 +40,11 @@ type instanceCreateCmd struct {
Labels map[string]string `cli-flag:"label" cli-usage:"instance label (format: key=value)"`
PrivateNetworks []string `cli-flag:"private-network" cli-usage:"instance Private Network NAME|ID (can be specified multiple times)"`
PrivateInstance bool `cli-flag:"private-instance" cli-usage:"enable private instance to be created"`
SSHKey string `cli-flag:"ssh-key" cli-usage:"SSH key to deploy on the instance"`
SSHKeys []string `cli-flag:"ssh-key" cli-usage:"SSH key to deploy on the instance (can be specified multiple times)"`
SecurityGroups []string `cli-flag:"security-group" cli-usage:"instance Security Group NAME|ID (can be specified multiple times)"`
Template string `cli-usage:"instance template NAME|ID"`
TemplateVisibility string `cli-usage:"instance template visibility (public|private)"`
Zone string `cli-short:"z" cli-usage:"instance zone"`
Zone v3.ZoneName `cli-short:"z" cli-usage:"instance zone"`
}

func (c *instanceCreateCmd) cmdAliases() []string { return gCreateAlias }
Expand Down Expand Up @@ -75,59 +74,85 @@ func (c *instanceCreateCmd) cmdRun(_ *cobra.Command, _ []string) error { //nolin
var (
singleUseSSHPrivateKey *rsa.PrivateKey
singleUseSSHPublicKey ssh.PublicKey
sshKey *egoscale.SSHKey
)
ctx := gContext
client, err := switchClientZoneV3(ctx, globalstate.EgoscaleV3Client, c.Zone)
if err != nil {
return err
}

instance := &egoscale.Instance{
DiskSize: &c.DiskSize,
IPv6Enabled: &c.IPv6,
Labels: func() (v *map[string]string) {
if len(c.Labels) > 0 {
return &c.Labels
}
return
}(),
Name: &c.Name,
SSHKey: utils.NonEmptyStringPtr(c.SSHKey),
var sshKeys []v3.SSHKey
for _, sshkeyName := range c.SSHKeys {
sshKeys = append(sshKeys, v3.SSHKey{Name: sshkeyName})
}

if c.PrivateInstance {
t := "none"
instance.PublicIPAssignment = &t
instanceReq := v3.CreateInstanceRequest{
DiskSize: c.DiskSize,
Ipv6Enabled: &c.IPv6,
Labels: c.Labels,
Name: c.Name,
SSHKeys: sshKeys,
}

ctx := exoapi.WithEndpoint(gContext, exoapi.NewReqEndpoint(account.CurrentAccount.Environment, c.Zone))
if c.PrivateInstance {
instanceReq.PublicIPAssignment = v3.PublicIPAssignmentNone
}

if l := len(c.AntiAffinityGroups); l > 0 {
antiAffinityGroupIDs := make([]string, l)
antiAffinityGroupIDs := make([]v3.AntiAffinityGroup, l)
af, err := client.ListAntiAffinityGroups(ctx)
if err != nil {
return fmt.Errorf("error listing Anti-Affinity Group: %w", err)
}
for i := range c.AntiAffinityGroups {
antiAffinityGroup, err := globalstate.EgoscaleClient.FindAntiAffinityGroup(ctx, c.Zone, c.AntiAffinityGroups[i])
antiAffinityGroup, err := af.FindAntiAffinityGroup(c.AntiAffinityGroups[i])
if err != nil {
return fmt.Errorf("error retrieving Anti-Affinity Group: %w", err)
}
antiAffinityGroupIDs[i] = *antiAffinityGroup.ID
antiAffinityGroupIDs[i] = antiAffinityGroup
}
instance.AntiAffinityGroupIDs = &antiAffinityGroupIDs

instanceReq.AntiAffinityGroups = antiAffinityGroupIDs
}

if c.DeployTarget != "" {
deployTarget, err := globalstate.EgoscaleClient.FindDeployTarget(ctx, c.Zone, c.DeployTarget)
targets, err := client.ListDeployTargets(ctx)
if err != nil {
return fmt.Errorf("error listing Deploy Target: %w", err)
}
deployTarget, err := targets.FindDeployTarget(c.DeployTarget)
if err != nil {
return fmt.Errorf("error retrieving Deploy Target: %w", err)
}
instance.DeployTargetID = deployTarget.ID
instanceReq.DeployTarget = &deployTarget
}

instanceType, err := globalstate.EgoscaleClient.FindInstanceType(ctx, c.Zone, c.InstanceType)
instanceTypes, err := client.ListInstanceTypes(ctx)
if err != nil {
return fmt.Errorf("error retrieving instance type: %w", err)
return fmt.Errorf("error listing instance type: %w", err)
}

// c.InstanceType is never empty
instanceType := utils.ParseInstanceType(c.InstanceType)
for i, it := range instanceTypes.InstanceTypes {
if it.Family == instanceType.Family && it.Size == instanceType.Size {
instanceReq.InstanceType = &instanceTypes.InstanceTypes[i]
pierre-emmanuelJ marked this conversation as resolved.
Show resolved Hide resolved
break
}
}
if instanceReq.InstanceType == nil {
return fmt.Errorf("error retrieving instance type %s: not found", c.InstanceType)
}
instance.InstanceTypeID = instanceType.ID

privateNetworks := make([]*egoscale.PrivateNetwork, len(c.PrivateNetworks))
privateNetworks := make([]v3.PrivateNetwork, len(c.PrivateNetworks))
if l := len(c.PrivateNetworks); l > 0 {
pNetworks, err := client.ListPrivateNetworks(ctx)
if err != nil {
return fmt.Errorf("error listing Private Network: %w", err)
}

for i := range c.PrivateNetworks {
privateNetwork, err := globalstate.EgoscaleClient.FindPrivateNetwork(ctx, c.Zone, c.PrivateNetworks[i])
privateNetwork, err := pNetworks.FindPrivateNetwork(c.PrivateNetworks[i])
if err != nil {
return fmt.Errorf("error retrieving Private Network: %w", err)
}
Expand All @@ -136,23 +161,26 @@ func (c *instanceCreateCmd) cmdRun(_ *cobra.Command, _ []string) error { //nolin
}

if l := len(c.SecurityGroups); l > 0 {
securityGroupIDs := make([]string, l)
sgs, err := client.ListSecurityGroups(ctx)
if err != nil {
return fmt.Errorf("error listing Security Group: %w", err)
}
instanceReq.SecurityGroups = make([]v3.SecurityGroup, l)
for i := range c.SecurityGroups {
securityGroup, err := globalstate.EgoscaleClient.FindSecurityGroup(ctx, c.Zone, c.SecurityGroups[i])
securityGroup, err := sgs.FindSecurityGroup(c.SecurityGroups[i])
if err != nil {
return fmt.Errorf("error retrieving Security Group: %w", err)
}
securityGroupIDs[i] = *securityGroup.ID
instanceReq.SecurityGroups[i] = securityGroup
}
instance.SecurityGroupIDs = &securityGroupIDs
}

if instance.SSHKey == nil && account.CurrentAccount.DefaultSSHKey != "" {
instance.SSHKey = &account.CurrentAccount.DefaultSSHKey
if instanceReq.SSHKeys == nil && account.CurrentAccount.DefaultSSHKey != "" {
instanceReq.SSHKeys = []v3.SSHKey{{Name: account.CurrentAccount.DefaultSSHKey}}
}

// Generating a single-use SSH key pair for this instance.
if instance.SSHKey == nil {
if instanceReq.SSHKeys == nil {
singleUseSSHPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return fmt.Errorf("error generating SSH private key: %w", err)
Expand All @@ -166,20 +194,30 @@ func (c *instanceCreateCmd) cmdRun(_ *cobra.Command, _ []string) error { //nolin
return fmt.Errorf("error generating SSH public key: %w", err)
}

sshKey, err = globalstate.EgoscaleClient.RegisterSSHKey(
sshKeyName := fmt.Sprintf("%s-%d", c.Name, time.Now().Unix())
op, err := client.RegisterSSHKey(
ctx,
c.Zone,
fmt.Sprintf("%s-%d", c.Name, time.Now().Unix()),
string(ssh.MarshalAuthorizedKey(singleUseSSHPublicKey)),
v3.RegisterSSHKeyRequest{
Name: sshKeyName,
PublicKey: string(ssh.MarshalAuthorizedKey(singleUseSSHPublicKey)),
},
)
if err != nil {
return fmt.Errorf("error registering SSH key: %w", err)
}
_, err = client.Wait(ctx, op, v3.OperationStateSuccess)
if err != nil {
return fmt.Errorf("error wait registering SSH key: %w", err)
}

instance.SSHKey = sshKey.Name
instanceReq.SSHKeys = []v3.SSHKey{{Name: sshKeyName}}
}

template, err := globalstate.EgoscaleClient.FindTemplate(ctx, c.Zone, c.Template, c.TemplateVisibility)
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",
Expand All @@ -188,24 +226,41 @@ func (c *instanceCreateCmd) cmdRun(_ *cobra.Command, _ []string) error { //nolin
c.Zone,
)
}
instance.TemplateID = template.ID
instanceReq.Template = &template

if c.CloudInitFile != "" {
userData, err := userdata.GetUserDataFromFile(c.CloudInitFile, c.CloudInitCompress)
if err != nil {
return fmt.Errorf("error parsing cloud-init user data: %w", err)
}
instance.UserData = &userData
instanceReq.UserData = userData
}

var instanceID v3.UUID
decorateAsyncOperation(fmt.Sprintf("Creating instance %q...", c.Name), func() {
instance, err = globalstate.EgoscaleClient.CreateInstance(ctx, c.Zone, instance)
var op *v3.Operation
op, err = client.CreateInstance(ctx, instanceReq)
if err != nil {
return
}

op, err = client.Wait(ctx, op, v3.OperationStateSuccess)
if err != nil {
return
}
if op.Reference != nil {
instanceID = op.Reference.ID
}

for _, p := range privateNetworks {
if err = globalstate.EgoscaleClient.AttachInstanceToPrivateNetwork(ctx, c.Zone, instance, p); err != nil {
op, err = client.AttachInstanceToPrivateNetwork(ctx, p.ID, v3.AttachInstanceToPrivateNetworkRequest{
Instance: &v3.AttachInstanceToPrivateNetworkRequestInstance{ID: instanceID},
})
if err != nil {
return
}
_, err = client.Wait(ctx, op)
if err != nil {
return
}
}
Expand All @@ -215,7 +270,7 @@ func (c *instanceCreateCmd) cmdRun(_ *cobra.Command, _ []string) error { //nolin
}

if singleUseSSHPrivateKey != nil {
privateKeyFilePath := exossh.GetInstanceSSHKeyPath(*instance.ID)
privateKeyFilePath := exossh.GetInstanceSSHKeyPath(instanceID.String())

if err = os.MkdirAll(path.Dir(privateKeyFilePath), 0o700); err != nil {
return fmt.Errorf("error writing SSH private key file: %w", err)
Expand All @@ -232,16 +287,22 @@ func (c *instanceCreateCmd) cmdRun(_ *cobra.Command, _ []string) error { //nolin
return fmt.Errorf("error writing SSH private key file: %w", err)
}

if err = globalstate.EgoscaleClient.DeleteSSHKey(ctx, c.Zone, sshKey); err != nil {
op, err := client.DeleteSSHKey(ctx, instanceReq.SSHKeys[0].Name)
if err != nil {
return fmt.Errorf("error deleting SSH key: %w", err)
}
_, err = client.Wait(ctx, op, v3.OperationStateSuccess)
if err != nil {
return fmt.Errorf("error wait deleting SSH key: %w", err)
}
}

if !globalstate.Quiet {
return (&instanceShowCmd{
cliCommandSettings: c.cliCommandSettings,
Instance: *instance.ID,
Zone: c.Zone,
Instance: instanceID.String(),
// migrate instanceShow to v3 to pass v3.ZoneName
pierre-emmanuelJ marked this conversation as resolved.
Show resolved Hide resolved
Zone: string(c.Zone),
}).cmdRun(nil, nil)
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/aws/smithy-go v1.1.0
github.com/dustin/go-humanize v1.0.1
github.com/exoscale/egoscale v0.102.4
github.com/exoscale/egoscale/v3 v3.1.0
github.com/exoscale/egoscale/v3 v3.1.1
github.com/exoscale/openapi-cli-generator v1.1.0
github.com/fatih/camelcase v1.0.0
github.com/google/uuid v1.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/exoscale/egoscale v0.102.4 h1:GBKsZMIOzwBfSu+4ZmWka3Ejf2JLiaBDHp4CQUgvp2E=
github.com/exoscale/egoscale v0.102.4/go.mod h1:ROSmPtle0wvf91iLZb09++N/9BH2Jo9XxIpAEumvocA=
github.com/exoscale/egoscale/v3 v3.1.0 h1:8MSA0j4TZbUiE6iIzTmoY0URa3RoGGuHhX5oamCql4o=
github.com/exoscale/egoscale/v3 v3.1.0/go.mod h1:lPsza7G+giSxdzvzaHSEcjEAYz/YTiu2bEEha9KVAc4=
github.com/exoscale/egoscale/v3 v3.1.1 h1:NwTlXE2sKe2kBWm+c3bsHV+aWDFiEJ9JQpS6X3j4wbc=
github.com/exoscale/egoscale/v3 v3.1.1/go.mod h1:lPsza7G+giSxdzvzaHSEcjEAYz/YTiu2bEEha9KVAc4=
github.com/exoscale/openapi-cli-generator v1.1.0 h1:fYjmPqHR5vxlOBrbvde7eo7bISNQIFxsGn4A5/acwKA=
github.com/exoscale/openapi-cli-generator v1.1.0/go.mod h1:TZBnbT7f3hJ5ImyUphJwRM+X5xF/zCQZ6o8a42gQeTs=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
Expand Down
20 changes: 20 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/hashicorp/go-multierror"

exoapi "github.com/exoscale/egoscale/v2/api"
v3 "github.com/exoscale/egoscale/v3"

"github.com/exoscale/cli/pkg/account"
v2 "github.com/exoscale/egoscale/v2"
Expand Down Expand Up @@ -236,3 +237,22 @@ func ForEachZone(zones []string, f func(zone string) error) error {

return meg.Wait().ErrorOrNil()
}

// ParseInstanceType returns an v3.InstanceType with family and name.
func ParseInstanceType(instanceType string) v3.InstanceType {
pierre-emmanuelJ marked this conversation as resolved.
Show resolved Hide resolved
var typeFamily, typeSize string

parts := strings.SplitN(instanceType, ".", 2)
if l := len(parts); l > 0 {
if l == 1 {
typeFamily, typeSize = "standard", strings.ToLower(parts[0])
} else {
typeFamily, typeSize = strings.ToLower(parts[0]), strings.ToLower(parts[1])
}
}

return v3.InstanceType{
Family: v3.InstanceTypeFamily(typeFamily),
Size: v3.InstanceTypeSize(typeSize),
}
}
30 changes: 30 additions & 0 deletions utils/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package utils

import (
"testing"

v3 "github.com/exoscale/egoscale/v3"
"github.com/stretchr/testify/assert"
)

func TestParseInstanceType(t *testing.T) {
testCases := []struct {
instanceType string
expectedFamily v3.InstanceTypeFamily
expectedSize v3.InstanceTypeSize
}{
{"standard.large", v3.InstanceTypeFamily("standard"), v3.InstanceTypeSize("large")},
{"gpu2.mega", v3.InstanceTypeFamily("gpu2"), v3.InstanceTypeSize("mega")},
{"colossus", v3.InstanceTypeFamily("standard"), v3.InstanceTypeSize("colossus")},
{"", v3.InstanceTypeFamily("standard"), v3.InstanceTypeSize("")},
{"invalid-format", v3.InstanceTypeFamily("standard"), v3.InstanceTypeSize("invalid-format")},
}

for _, tc := range testCases {
t.Run(tc.instanceType, func(t *testing.T) {
result := ParseInstanceType(tc.instanceType)
assert.Equal(t, tc.expectedFamily, result.Family)
assert.Equal(t, tc.expectedSize, result.Size)
})
}
}
Loading
Loading