Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ docker-machine create -d linode --linode-token=<linode-token> linode
| `linode-swap-size` | `LINODE_SWAP_SIZE` | `512` | The amount of swap space provisioned on the Linode Instance
| `linode-stackscript` | `LINODE_STACKSCRIPT` | None | Specifies the Linode StackScript to use to create the instance, either by numeric ID, or using the form *username*/*label*.
| `linode-stackscript-data` | `LINODE_STACKSCRIPT_DATA` | None | A JSON string specifying data that is passed (via UDF) to the selected StackScript.
| `linode-user-data` | `LINODE_USER_DATA` | None | Path to a cloud-init user-data file passed to the Linode Metadata service. File contents are base64-encoded automatically.
| `linode-create-private-ip` | `LINODE_CREATE_PRIVATE_IP` | None | A flag specifying to create private IP for the Linode instance.
| `linode-tags` | `LINODE_TAGS` | None | A comma separated list of tags to apply to the Linode resource
| `linode-ua-prefix` | `LINODE_UA_PREFIX` | None | Prefix the User-Agent in Linode API calls with some 'product/version'
Expand Down
43 changes: 42 additions & 1 deletion pkg/drivers/linode/linode.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ type Driver struct {
StackScriptLabel string
StackScriptData map[string]string

Tags string
// UserData contains base64-encoded cloud-init user data for the Linode Metadata service.
UserData string
Tags string
}

// VERSION represents the semver version of the package
Expand Down Expand Up @@ -221,6 +223,11 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
Usage: "A JSON string specifying data for the selected StackScript",
Value: "",
},
mcnflag.StringFlag{
EnvVar: "LINODE_USER_DATA",
Name: "linode-user-data",
Usage: "Path to a cloud-init user-data file for the Linode Metadata service",
},
mcnflag.BoolFlag{
EnvVar: "LINODE_CREATE_PRIVATE_IP",
Name: "linode-create-private-ip",
Expand Down Expand Up @@ -279,6 +286,16 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
d.UserAgentPrefix = flags.String("linode-ua-prefix")
d.Tags = flags.String("linode-tags")

userData := flags.String("linode-user-data")
if userData != "" {
encodedUserData, err := encodeUserData(userData)
if err != nil {
return err
}

d.UserData = encodedUserData
}

d.SetSwarmConfigFromFlags(flags)

if d.APIToken == "" {
Expand Down Expand Up @@ -323,6 +340,24 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
return nil
}

func encodeUserData(userData string) (string, error) {
if userData == "" {
return "", nil
}

path := strings.TrimSpace(userData)
if path == "" {
return "", fmt.Errorf("--linode-user-data requires a file path")
}

content, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("failed to read user data from --linode-user-data file %q: %w", path, err)
}

return base64.StdEncoding.EncodeToString(content), nil
}

// PreCreateCheck allows for pre-create operations to make sure a driver is ready for creation
func (d *Driver) PreCreateCheck() error {
// TODO(displague) linode-stackscript-file should be read and uploaded (private), then used for boot.
Expand Down Expand Up @@ -426,6 +461,12 @@ func (d *Driver) Create() error {
log.Infof("Using StackScript %d: %s/%s", d.StackScriptID, d.StackScriptUser, d.StackScriptLabel)
}

if d.UserData != "" {
createOpts.Metadata = &linodego.InstanceMetadataOptions{
UserData: d.UserData,
}
}

linode, err := client.CreateInstance(context.TODO(), createOpts)
if err != nil {
return err
Expand Down
64 changes: 64 additions & 0 deletions pkg/drivers/linode/linode_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package linode

import (
"encoding/base64"
"net"
"os"
"path/filepath"
"reflect"
"testing"

Expand All @@ -27,6 +30,67 @@ func TestSetConfigFromFlags(t *testing.T) {
assert.Empty(t, checkFlags.InvalidFlags)
}

func TestSetConfigFromFlagsUserDataFile(t *testing.T) {
driver := NewDriver("", "")

dir := t.TempDir()
userDataPath := filepath.Join(dir, "user-data.yaml")
userData := "#cloud-config\npackages:\n - curl\n"
if err := os.WriteFile(userDataPath, []byte(userData), 0o600); err != nil {
t.Fatalf("failed to write user data fixture: %s", err)
}

checkFlags := &drivers.CheckDriverOptions{
FlagsValues: map[string]interface{}{
"linode-token": "PROJECT",
"linode-root-pass": "ROOTPASS",
"linode-user-data": userDataPath,
},
CreateFlags: driver.GetCreateFlags(),
}

err := driver.SetConfigFromFlags(checkFlags)

assert.NoError(t, err)
assert.Equal(t, base64.StdEncoding.EncodeToString([]byte(userData)), driver.UserData)
}

func TestSetConfigFromFlagsUserDataMissingFile(t *testing.T) {
driver := NewDriver("", "")

checkFlags := &drivers.CheckDriverOptions{
FlagsValues: map[string]interface{}{
"linode-token": "PROJECT",
"linode-root-pass": "ROOTPASS",
"linode-user-data": "/does/not/exist",
},
CreateFlags: driver.GetCreateFlags(),
}

err := driver.SetConfigFromFlags(checkFlags)

assert.Error(t, err)
assert.Contains(t, err.Error(), "--linode-user-data")
}

func TestSetConfigFromFlagsUserDataEmptyPath(t *testing.T) {
driver := NewDriver("", "")

checkFlags := &drivers.CheckDriverOptions{
FlagsValues: map[string]interface{}{
"linode-token": "PROJECT",
"linode-root-pass": "ROOTPASS",
"linode-user-data": " ",
},
CreateFlags: driver.GetCreateFlags(),
}

err := driver.SetConfigFromFlags(checkFlags)

assert.Error(t, err)
assert.Contains(t, err.Error(), "--linode-user-data")
}

func TestPrivateIP(t *testing.T) {
ip := net.IP{}
for _, addr := range [][]byte{
Expand Down