Skip to content

Commit

Permalink
Instance Create: Migrate to egoscale v3 and add multiple sshkeys
Browse files Browse the repository at this point in the history
Signed-off-by: Pierre-Emmanuel Jacquier <[email protected]>
  • Loading branch information
pierre-emmanuelJ committed Jul 15, 2024
1 parent a0ac69a commit 36e7d44
Show file tree
Hide file tree
Showing 13 changed files with 917 additions and 496 deletions.
158 changes: 106 additions & 52 deletions cmd/instance_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import (
"github.com/exoscale/cli/pkg/output"
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 +39,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 keys to deploy on the instance"`
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 +73,79 @@ 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 listing instance type: %w", err)
}

instanceType, err := instanceTypes.FindInstanceType(c.InstanceType)
if err != nil {
return fmt.Errorf("error retrieving instance type: %w", err)
}
instance.InstanceTypeID = instanceType.ID
instanceReq.InstanceType = &instanceType

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 +154,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 +187,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 +219,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 +263,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 +280,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, 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
Zone: string(c.Zone),
}).cmdRun(nil, nil)
}

Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.2.0
github.com/aws/smithy-go v1.1.0
github.com/dustin/go-humanize v1.0.1
github.com/exoscale/egoscale v0.102.4-0.20240506093113-3ae83713b097
github.com/exoscale/egoscale v0.102.5-0.20240712161721-275fc20694fd
github.com/exoscale/egoscale/v3 v3.1.1-0.20240712161721-275fc20694fd
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
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,10 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/exoscale/egoscale v0.102.4-0.20240506093113-3ae83713b097 h1:nr8gLscG7DHFUIPuyLebhrtGcd6OM+LUjcAX3erF7Y4=
github.com/exoscale/egoscale v0.102.4-0.20240506093113-3ae83713b097/go.mod h1:20KZJQlNJktQon39FWSKXET1h+OicXsyLzNdjc7bNuY=
github.com/exoscale/egoscale v0.102.5-0.20240712161721-275fc20694fd h1:2rQ9GatI7MzMwfx1hX0i5znmjk8F9XCyJX/DTz9UCjE=
github.com/exoscale/egoscale v0.102.5-0.20240712161721-275fc20694fd/go.mod h1:xKtCzfF+1O6sKtMb7QANYrTher0EFhkmw8LQLu7Scm0=
github.com/exoscale/egoscale/v3 v3.1.1-0.20240712161721-275fc20694fd h1:RhnSciyRNzsa2qdAr7k8S7diePF8bx6Nyg/nmTQXMjI=
github.com/exoscale/egoscale/v3 v3.1.1-0.20240712161721-275fc20694fd/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
2 changes: 2 additions & 0 deletions vendor/github.com/exoscale/egoscale/v2/staticcheck.conf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 36e7d44

Please sign in to comment.