Skip to content

Commit

Permalink
fix: use devpod agent to install docker
Browse files Browse the repository at this point in the history
  • Loading branch information
mrsimonemms committed Jun 18, 2023
1 parent 6dba4d8 commit c75fbe6
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 38 deletions.
68 changes: 49 additions & 19 deletions cmd/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,66 @@ limitations under the License.
package cmd

import (
"context"
"fmt"
"os"

"github.com/loft-sh/devpod/pkg/ssh"
"github.com/mrsimonemms/devpod-provider-hetzner/pkg/hetzner"
"github.com/mrsimonemms/devpod-provider-hetzner/pkg/options"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

// commandCmd represents the command command
var commandCmd = &cobra.Command{
Use: "command",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("command called")
Short: "Run a command on the instance",
RunE: func(cmd *cobra.Command, args []string) error {
options, err := options.FromEnv(false)
if err != nil {
return err
}

ctx := context.Background()

command := os.Getenv("COMMAND")
if command == "" {
return fmt.Errorf("command environment variable is missing")
}

// Get private key
privateKey, err := ssh.GetPrivateKeyRawBase(options.MachineFolder)
if err != nil {
return fmt.Errorf("load private key: %w", err)
}

// Create SSH client
server, err := hetzner.NewHetzner(options.Token).GetByName(ctx, options.MachineID)
if err != nil {
return err
} else if server == nil {
return fmt.Errorf("droplet not found")
}

// Call external address
sshClient, err := ssh.NewSSHClient("devpod", fmt.Sprintf("%s:22", server.PublicNet.IPv4.IP), privateKey)
if err != nil {
return errors.Wrap(err, "create ssh client")
}
defer func() {
err = sshClient.Close()
}()

// Run command
if err := ssh.Run(ctx, sshClient, command, os.Stdin, os.Stdout, os.Stderr); err != nil {
return err
}

return err
},
}

func init() {
rootCmd.AddCommand(commandCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// commandCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// commandCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
4 changes: 2 additions & 2 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func createOrStartServer(cmd *cobra.Command, args []string) error {
ctx := context.Background()
h := hetzner.NewHetzner(options.Token)

req, publicKey, err := h.BuildServerOptions(ctx, options)
req, publicKey, privateKey, err := h.BuildServerOptions(ctx, options)
if err != nil {
return err
}
Expand All @@ -54,7 +54,7 @@ func createOrStartServer(cmd *cobra.Command, args []string) error {
return errors.Wrap(err, "parse disk size")
}

return h.Create(ctx, req, diskSize, *publicKey)
return h.Create(ctx, req, diskSize, *publicKey, privateKey)
}

func init() {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/loft-sh/devpod v0.1.9
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.7.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down
93 changes: 76 additions & 17 deletions pkg/hetzner/hetzner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@ import (
"context"
"embed"
"encoding/base64"
"fmt"
"strconv"
"text/template"
"time"

"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/loft-sh/devpod/pkg/client"
"github.com/loft-sh/devpod/pkg/log"
"github.com/loft-sh/devpod/pkg/ssh"
"github.com/mrsimonemms/devpod-provider-hetzner/pkg/options"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)

//go:embed cloud-config.yaml
var cloudConfig embed.FS

type cloudInit struct {
Status string `json:"status"`
}

type Hetzner struct {
client *hcloud.Client
}
Expand All @@ -29,40 +36,45 @@ func NewHetzner(token string) *Hetzner {
}
}

func (h *Hetzner) BuildServerOptions(ctx context.Context, opts *options.Options) (*hcloud.ServerCreateOpts, *string, error) {
func (h *Hetzner) BuildServerOptions(ctx context.Context, opts *options.Options) (*hcloud.ServerCreateOpts, *string, []byte, error) {
publicKeyBase, err := ssh.GetPublicKeyBase(opts.MachineFolder)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}

publicKey, err := base64.StdEncoding.DecodeString(publicKeyBase)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}

privateKey, err := ssh.GetPrivateKeyRawBase(opts.MachineFolder)
if err != nil {
return nil, nil, nil, err
}

location, _, err := h.client.Location.GetByName(ctx, opts.Region)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if location == nil {
return nil, nil, ErrUnknownRegion
return nil, nil, nil, ErrUnknownRegion
}

serverType, _, err := h.client.ServerType.GetByName(ctx, opts.MachineType)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if serverType == nil {
return nil, nil, ErrUnknownMachineID
return nil, nil, nil, ErrUnknownMachineID
}

// @todo(sje): work out if DevPod handles different architectures
image, _, err := h.client.Image.GetByNameAndArchitecture(ctx, opts.DiskImage, hcloud.ArchitectureX86)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if image == nil {
return nil, nil, ErrUnknownDiskImage
return nil, nil, nil, ErrUnknownDiskImage
}

return &hcloud.ServerCreateOpts{
Expand All @@ -73,17 +85,21 @@ func (h *Hetzner) BuildServerOptions(ctx context.Context, opts *options.Options)
Labels: map[string]string{
"type": "devpod",
},
}, hcloud.Ptr[string](string(publicKey)), nil
}, hcloud.Ptr[string](string(publicKey)), privateKey, nil
}

func (h *Hetzner) Create(ctx context.Context, req *hcloud.ServerCreateOpts, diskSize int, publicKey string) error {
func (h *Hetzner) Create(ctx context.Context, req *hcloud.ServerCreateOpts, diskSize int, publicKey string, privateKeyFile []byte) error {
log.Default.Debug("Creating DevPod instance")

volume, err := h.volumeByName(ctx, req.Name)
if err != nil {
return err
}

if volume == nil {
// Create the volume as it doesn't exist
log.Default.Debug("Creating a new volume")

v, _, err := h.client.Volume.Create(ctx, hcloud.VolumeCreateOpts{
Location: req.Location,
Name: req.Name,
Expand All @@ -98,6 +114,8 @@ func (h *Hetzner) Create(ctx context.Context, req *hcloud.ServerCreateOpts, disk
return err
}

log.Default.Debug("Volume successfully created")

volume = v.Volume
}

Expand All @@ -116,10 +134,53 @@ func (h *Hetzner) Create(ctx context.Context, req *hcloud.ServerCreateOpts, disk
},
}

// Create the volume
_, _, err = h.client.Server.Create(ctx, *req)
// Create the server
log.Default.Debug("Creating a new server")
server, _, err := h.client.Server.Create(ctx, *req)
if err != nil {
return err
}

return err
log.Default.Debug("Server created - waiting until provisioned")

for {
time.Sleep(time.Second)

log.Default.Debug("Checking server provision status")

// Check the server is provisioned - this runs "ssh user@path cloud-init status"
sshClient, err := ssh.NewSSHClient("devpod", fmt.Sprintf("%s:22", server.Server.PublicNet.IPv4.IP), privateKeyFile)
if err != nil {
log.Default.Debug("Unable to connect to server")
continue
}
defer func() {
err = sshClient.Close()
}()

buf := new(bytes.Buffer)
if err := ssh.Run(ctx, sshClient, "cloud-init status", &bytes.Buffer{}, buf, &bytes.Buffer{}); err != nil {
log.Default.Debug("Error retrieving cloud-init status")
continue
}

var status cloudInit
if err := yaml.Unmarshal(buf.Bytes(), &status); err != nil {
log.Default.Debug("Unable to parse cloud-init YAML")
continue
}

if status.Status == "done" {
// The server is ready
break
}

log.Default.Debug("Server not yet provisioned")
}

log.Default.Debug("Server provisioned")

return nil
}

func (h *Hetzner) Delete(ctx context.Context, name string) error {
Expand All @@ -143,7 +204,7 @@ func (h *Hetzner) Delete(ctx context.Context, name string) error {
volume, err = h.volumeByName(ctx, name)
if err != nil {
return err
} else if volume.Server == nil {
} else if volume == nil || volume.Server == nil {
break
}
}
Expand Down Expand Up @@ -214,8 +275,6 @@ func (h *Hetzner) Status(ctx context.Context, name string) (client.Status, error
return client.StatusBusy, nil
}

// @todo(sje): do we need to check if the cloud-init script is finished? "ssh user@path cloud-init status"

return client.StatusRunning, nil
}

Expand Down

0 comments on commit c75fbe6

Please sign in to comment.