diff --git a/builder/exoscale/builder_test.go b/builder/exoscale/builder_test.go index bdf789c9..e340762d 100644 --- a/builder/exoscale/builder_test.go +++ b/builder/exoscale/builder_test.go @@ -1,6 +1,7 @@ package exoscale import ( + "encoding/base64" "math/rand" "net" "os" @@ -34,6 +35,9 @@ var ( testTemplateID = new(testSuite).randomID() testTemplateName = "packer-plugin-test-" + new(testSuite).randomString(6) testTemplateZones = []string{"ch-gva-2", "ch-dk-2"} + testUserData = "echo test > /etc/test.txt" + testUserDataBase64 = base64.StdEncoding.EncodeToString([]byte(testUserData)) + testUserDataFile = "userdata.txt" testSeededRand = rand.New(rand.NewSource(time.Now().UnixNano())) ) diff --git a/builder/exoscale/config.go b/builder/exoscale/config.go index da06284a..b28bc684 100644 --- a/builder/exoscale/config.go +++ b/builder/exoscale/config.go @@ -4,6 +4,7 @@ package exoscale import ( "fmt" + "os" "reflect" "github.com/hashicorp/hcl/v2/hcldec" @@ -50,6 +51,8 @@ type Config struct { TemplateMaintainer string `mapstructure:"template_maintainer"` TemplateVersion string `mapstructure:"template_version"` TemplateBuild string `mapstructure:"template_build"` + UserData string `mapstructure:"user_data"` + UserDataFile string `mapstructure:"user_data_file"` // Deprecated TemplateZone string `mapstructure:"template_zone"` @@ -102,6 +105,14 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } } + if config.UserData != "" && config.UserDataFile != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified.")) + } else if config.UserDataFile != "" { + if _, err := os.Stat(config.UserDataFile); err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("user_data_file not found: %s", config.UserDataFile)) + } + } + if es := config.Comm.Prepare(&config.ctx); len(es) > 0 { errs = packer.MultiErrorAppend(errs, es...) } diff --git a/builder/exoscale/config_test.go b/builder/exoscale/config_test.go index f80e82f2..6bcbc93e 100644 --- a/builder/exoscale/config_test.go +++ b/builder/exoscale/config_test.go @@ -1,5 +1,7 @@ package exoscale +import "os" + var ( testConfigAPIKey = "EXOabcdef0123456789abcdef01" testConfigAPISecret = "ABCDEFGHIJKLMNOPRQSTUVWXYZ0123456789abcdefg" @@ -9,6 +11,8 @@ var ( testConfigTemplateZones = []string{"ch-gva-2", "ch-dk-2"} testConfigTemplateName = "test-packer" testConfigSSHUsername = "ubuntu" + testConfigUserData = "sed -i -E 's/#?PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config" + testConfigUserDataFile = "disable_ssh_password_auth.sh" // Deprecated testConfigTemplateZone = "ch-dk-2" ) @@ -27,6 +31,7 @@ func (ts *testSuite) TestNewConfig() { "template_name": testConfigTemplateName, "template_zones": testConfigTemplateZones, "ssh_username": testConfigSSHUsername, + "user_data": testConfigUserData, }}...) ts.Require().NoError(err) ts.Require().NotNil(config) @@ -39,6 +44,38 @@ func (ts *testSuite) TestNewConfig() { ts.Require().Equal(testConfigSnapshotDownloadPath, config.SnapshotDownloadPath) ts.Require().Equal(testConfigTemplateZones[0], config.InstanceZone) ts.Require().Equal(defaultTemplateBootMode, config.TemplateBootMode) + ts.Require().Equal(testConfigUserData, config.UserData) + + _, _, err = NewConfig([]interface{}{map[string]interface{}{ + // Minimal configuration + "api_key": testConfigAPIKey, + "api_secret": testConfigAPISecret, + "instance_template": testConfigInstanceTemplate, + "template_name": testConfigTemplateName, + "template_zones": testConfigTemplateZones, + "ssh_username": testConfigSSHUsername, + "user_data": testConfigUserData, + "user_data_file": testConfigUserDataFile, + }}...) + ts.Require().ErrorContains(err, "Only one of user_data or user_data_file can be specified.") + + tmpFile, err := os.CreateTemp(os.TempDir(), testConfigUserDataFile) + ts.Require().NoError(err, "unable to create temporary file") + ts.Require().NoError(tmpFile.Close()) + ts.Require().FileExists(tmpFile.Name()) + + config, _, err = NewConfig([]interface{}{map[string]interface{}{ + // Minimal configuration + "api_key": testConfigAPIKey, + "api_secret": testConfigAPISecret, + "instance_template": testConfigInstanceTemplate, + "template_name": testConfigTemplateName, + "template_zones": testConfigTemplateZones, + "ssh_username": testConfigSSHUsername, + "user_data_file": tmpFile.Name(), + }}...) + ts.Require().NoError(err) + ts.Require().Equal(tmpFile.Name(), config.UserDataFile) } func (ts *testSuite) TestNewConfigDeprecated() { diff --git a/builder/exoscale/step_create_instance.go b/builder/exoscale/step_create_instance.go index 6fcf62bc..6bb3de6f 100644 --- a/builder/exoscale/step_create_instance.go +++ b/builder/exoscale/step_create_instance.go @@ -2,7 +2,9 @@ package exoscale import ( "context" + "encoding/base64" "fmt" + "os" egoscale "github.com/exoscale/egoscale/v2" exoapi "github.com/exoscale/egoscale/v2/api" @@ -104,6 +106,26 @@ func (s *stepCreateInstance) Run(ctx context.Context, state multistep.StateBag) instance.SecurityGroupIDs = &securityGroupIDs } + userData := s.builder.config.UserData + if s.builder.config.UserDataFile != "" { + contents, err := os.ReadFile(s.builder.config.UserDataFile) + if err != nil { + ui.Error(fmt.Sprintf("Unable to read user data file: %v", err)) + return multistep.ActionHalt + } + + userData = string(contents) + } + + if userData != "" { + // Test if it is encoded already, and if not, encode it + if _, err := base64.StdEncoding.DecodeString(userData); err != nil { + userData = base64.StdEncoding.EncodeToString([]byte(userData)) + } + + instance.UserData = &userData + } + instance, err = s.builder.exo.CreateInstance(ctx, s.builder.config.InstanceZone, instance) if err != nil { ui.Error(fmt.Sprintf("Unable to create compute instance: %v", err)) diff --git a/builder/exoscale/step_create_instance_test.go b/builder/exoscale/step_create_instance_test.go index 6ac30715..22fed2f4 100644 --- a/builder/exoscale/step_create_instance_test.go +++ b/builder/exoscale/step_create_instance_test.go @@ -2,6 +2,7 @@ package exoscale import ( "context" + "os" "github.com/hashicorp/packer-plugin-sdk/multistep" "github.com/hashicorp/packer-plugin-sdk/packer" @@ -28,6 +29,14 @@ func (ts *testSuite) TestStepCreateInstance_Run() { instancePrivateNetworkAttached bool ) + tmpUserDataFile, err := os.CreateTemp(os.TempDir(), testUserDataFile) + ts.Require().NoError(err, "unable to create temporary userdata file") + ts.Require().FileExists(tmpUserDataFile.Name()) + _, err = tmpUserDataFile.WriteString(testUserData) + ts.Require().NoError(err) + ts.Require().NoError(tmpUserDataFile.Close()) + testConfig.UserDataFile = tmpUserDataFile.Name() + testInstance := &egoscale.Instance{ ID: &testInstanceID, Name: &testInstanceName, @@ -99,6 +108,7 @@ func (ts *testSuite) TestStepCreateInstance_Run() { SSHKey: &testConfig.InstanceSSHKey, SecurityGroupIDs: &[]string{testInstanceSecurityGroupID}, TemplateID: &testTemplateID, + UserData: &testUserDataBase64, }, args.Get(2)) instanceCreated = true diff --git a/docs/builders/exoscale.md b/docs/builders/exoscale.md index 73d8404a..1fa3dbf2 100644 --- a/docs/builders/exoscale.md +++ b/docs/builders/exoscale.md @@ -83,6 +83,13 @@ other OS, we recommend using the [QEMU][packerqemu] plugin combined with the - `template_disable_sshkey` (boolean) - Whether the template should disable SSH key installation during Compute instance creation. Defaults to `false`. +- `user_data` (string) - User data to apply when launching the instance. Note + that you need to be careful about escaping characters due to the templates + being JSON. See [documentation][cloudinit] to learn more about user data. + +- `user_data_file` (string) - The path to a file that will be used for the user + data when launching the instance. + In addition to plugin-specific configuration parameters, you can also adjust the [SSH communicator][packerssh] settings to configure how Packer will log into the Compute instance. @@ -119,3 +126,4 @@ build { [packerssh]: https://www.packer.io/docs/communicators/ssh/ [zones]: https://www.exoscale.com/datacenters/ [packerqemu]: https://www.packer.io/plugins/builders/qemu +[cloudinit]: https://community.exoscale.com/documentation/compute/cloud-init/