diff --git a/cloudstack/data_source_cloudstack_user_data.go b/cloudstack/data_source_cloudstack_user_data.go new file mode 100644 index 00000000..cd5c668c --- /dev/null +++ b/cloudstack/data_source_cloudstack_user_data.go @@ -0,0 +1,136 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "encoding/base64" + "fmt" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackUserData() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudstackUserDataRead, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "account": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "params": { + Type: schema.TypeString, + Computed: true, + }, + + "domain": { + Type: schema.TypeString, + Computed: true, + }, + + "domain_id": { + Type: schema.TypeString, + Computed: true, + }, + + "account_id": { + Type: schema.TypeString, + Computed: true, + }, + + "user_data": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceCloudstackUserDataRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.User.NewListUserDataParams() + p.SetName(d.Get("name").(string)) + + if account, ok := d.GetOk("account"); ok { + p.SetAccount(account.(string)) + } + + if project, ok := d.GetOk("project"); ok { + if project.(string) != "" { + projectid, retrieveErr := retrieveID(cs, "project", project.(string)) + if retrieveErr != nil { + return retrieveErr.Error() + } + p.SetProjectid(projectid) + } + } + + resp, err := cs.User.ListUserData(p) + if err != nil { + return fmt.Errorf("Error listing UserData: %s", err) + } + + if resp.Count == 0 || len(resp.UserData) == 0 { + return fmt.Errorf("UserData %s not found", d.Get("name").(string)) + } + + if resp.Count > 1 && len(resp.UserData) > 1 { + return fmt.Errorf("Multiple UserData entries found for name %s", d.Get("name").(string)) + } + + userData := resp.UserData[0] + + d.SetId(userData.Id) + d.Set("name", userData.Name) + d.Set("account", userData.Account) + d.Set("account_id", userData.Accountid) + d.Set("domain", userData.Domain) + d.Set("domain_id", userData.Domainid) + d.Set("params", userData.Params) + + if userData.Project != "" { + d.Set("project", userData.Project) + } + + if userData.Userdata != "" { + decoded, err := base64.StdEncoding.DecodeString(userData.Userdata) + if err != nil { + d.Set("user_data", userData.Userdata) + } else { + d.Set("user_data", string(decoded)) + } + } + + return nil +} diff --git a/cloudstack/data_source_cloudstack_user_data_test.go b/cloudstack/data_source_cloudstack_user_data_test.go new file mode 100644 index 00000000..935820cb --- /dev/null +++ b/cloudstack/data_source_cloudstack_user_data_test.go @@ -0,0 +1,57 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDataSourceCloudStackUserData_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackUserDataDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceCloudStackUserDataConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.cloudstack_user_data.test", "name", "terraform-test-userdata"), + resource.TestCheckResourceAttr("data.cloudstack_user_data.test", "user_data", "#!/bin/bash\\necho 'Hello World'\\n"), + ), + }, + }, + }) +} + +const testAccDataSourceCloudStackUserDataConfig = ` +resource "cloudstack_user_data" "test" { + name = "terraform-test-userdata" + user_data = <<-EOF + #!/bin/bash + echo 'Hello World' + EOF +} + +data "cloudstack_user_data" "test" { + name = cloudstack_user_data.test.name +} +` diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 0e0f6cff..eb741c62 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -83,6 +83,7 @@ func Provider() *schema.Provider { "cloudstack_autoscale_vm_profile": dataSourceCloudstackAutoscaleVMProfile(), "cloudstack_condition": dataSourceCloudstackCondition(), "cloudstack_counter": dataSourceCloudstackCounter(), + "cloudstack_user_data": dataSourceCloudstackUserData(), "cloudstack_template": dataSourceCloudstackTemplate(), "cloudstack_ssh_keypair": dataSourceCloudstackSSHKeyPair(), "cloudstack_instance": dataSourceCloudstackInstance(), @@ -155,6 +156,8 @@ func Provider() *schema.Provider { "cloudstack_account": resourceCloudStackAccount(), "cloudstack_project": resourceCloudStackProject(), "cloudstack_user": resourceCloudStackUser(), + "cloudstack_user_data": resourceCloudStackUserData(), + "cloudstack_user_data_template_link": resourceCloudStackUserDataTemplateLink(), "cloudstack_domain": resourceCloudStackDomain(), "cloudstack_network_service_provider": resourceCloudStackNetworkServiceProvider(), "cloudstack_role": resourceCloudStackRole(), diff --git a/cloudstack/resource_cloudstack_user_data.go b/cloudstack/resource_cloudstack_user_data.go new file mode 100644 index 00000000..61f6f623 --- /dev/null +++ b/cloudstack/resource_cloudstack_user_data.go @@ -0,0 +1,232 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "encoding/base64" + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudStackUserData() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackUserDataCreate, + Read: resourceCloudStackUserDataRead, + Update: resourceCloudStackUserDataUpdate, + Delete: resourceCloudStackUserDataDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "user_data": { + Type: schema.TypeString, + Required: true, + }, + + "account": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "params": { + Type: schema.TypeString, + Optional: true, + }, + + "domain": { + Type: schema.TypeString, + Computed: true, + }, + + "domain_id": { + Type: schema.TypeString, + Computed: true, + }, + + "account_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackUserDataCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + userData := d.Get("user_data").(string) + + // Encode user data as base64 if not already encoded + ud, err := getUserData(userData) + if err != nil { + return fmt.Errorf("Error encoding user data: %s", err) + } + + // Validate user data size (CloudStack API limitation) + if len(ud) > 1048576 { // 1MB in bytes for base64 encoded content + return fmt.Errorf("UserData is too large: %d bytes (max 1MB for base64 encoded content). Consider reducing content or using CloudStack global setting vm.userdata.max.length", len(ud)) + } + + // Create a new parameter struct + p := cs.User.NewRegisterUserDataParams(name, ud) + + // Set optional parameters + if account, ok := d.GetOk("account"); ok { + p.SetAccount(account.(string)) + } + + if params, ok := d.GetOk("params"); ok { + p.SetParams(params.(string)) + } + + // If there is a project supplied, we retrieve and set the project id + if err := setProjectid(p, cs, d); err != nil { + return err + } + + log.Printf("[DEBUG] Registering UserData %s", name) + r, err := cs.User.RegisterUserData(p) + if err != nil { + return fmt.Errorf("Error registering UserData %s: %s", name, err) + } + + d.SetId(r.Id) + + return resourceCloudStackUserDataRead(d, meta) +} + +func resourceCloudStackUserDataRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the UserData details + userData, count, err := cs.User.GetUserDataByID( + d.Id(), + cloudstack.WithProject(d.Get("project").(string)), + ) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] UserData %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + + return err + } + + // Update the config + d.Set("name", userData.Name) + d.Set("account", userData.Account) + d.Set("account_id", userData.Accountid) + d.Set("domain", userData.Domain) + d.Set("domain_id", userData.Domainid) + d.Set("params", userData.Params) + + // Decode and set the user data + if userData.Userdata != "" { + decoded, err := base64.StdEncoding.DecodeString(userData.Userdata) + if err != nil { + // If decoding fails, assume it's already plain text + d.Set("user_data", userData.Userdata) + } else { + d.Set("user_data", string(decoded)) + } + } + + return nil +} + +func resourceCloudStackUserDataUpdate(d *schema.ResourceData, meta interface{}) error { + name := d.Get("name").(string) + + if d.HasChange("user_data") || d.HasChange("params") { + // For updates, we need to delete and recreate as CloudStack doesn't have an update API + log.Printf("[DEBUG] UserData %s has changes, recreating", name) + + // Validate user data size before proceeding with update + if d.HasChange("user_data") { + userData := d.Get("user_data").(string) + ud, err := getUserData(userData) + if err != nil { + return fmt.Errorf("Error encoding user data: %s", err) + } + if len(ud) > 1048576 { // 1MB in bytes for base64 encoded content + return fmt.Errorf("UserData is too large: %d bytes (max 1MB for base64 encoded content). Consider reducing content or using CloudStack global setting vm.userdata.max.length", len(ud)) + } + } + + // Delete the old UserData + if err := resourceCloudStackUserDataDelete(d, meta); err != nil { + return err + } + + // Create new UserData + return resourceCloudStackUserDataCreate(d, meta) + } + + return resourceCloudStackUserDataRead(d, meta) +} + +func resourceCloudStackUserDataDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.User.NewDeleteUserDataParams(d.Id()) + + // Set optional parameters if they were used during creation + if account, ok := d.GetOk("account"); ok { + p.SetAccount(account.(string)) + } + + if project, ok := d.GetOk("project"); ok { + if !cloudstack.IsID(project.(string)) { + id, _, err := cs.Project.GetProjectID(project.(string)) + if err != nil { + return err + } + p.SetProjectid(id) + } else { + p.SetProjectid(project.(string)) + } + } + + log.Printf("[DEBUG] Deleting UserData %s", d.Get("name").(string)) + _, err := cs.User.DeleteUserData(p) + if err != nil { + return fmt.Errorf("Error deleting UserData: %s", err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_user_data_template_link.go b/cloudstack/resource_cloudstack_user_data_template_link.go new file mode 100644 index 00000000..9021810d --- /dev/null +++ b/cloudstack/resource_cloudstack_user_data_template_link.go @@ -0,0 +1,229 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudStackUserDataTemplateLink() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackUserDataTemplateLinkCreate, + Read: resourceCloudStackUserDataTemplateLinkRead, + Update: resourceCloudStackUserDataTemplateLinkUpdate, + Delete: resourceCloudStackUserDataTemplateLinkDelete, + + Schema: map[string]*schema.Schema{ + "template_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"iso_id"}, + ForceNew: true, + }, + + "iso_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"template_id"}, + ForceNew: true, + }, + + "user_data_id": { + Type: schema.TypeString, + Optional: true, + }, + + "user_data_policy": { + Type: schema.TypeString, + Optional: true, + Default: "ALLOWOVERRIDE", + ValidateFunc: validateUserDataPolicy, + }, + + // Computed attributes from template response + "name": { + Type: schema.TypeString, + Computed: true, + }, + + "display_text": { + Type: schema.TypeString, + Computed: true, + }, + + "is_ready": { + Type: schema.TypeBool, + Computed: true, + }, + + "template_type": { + Type: schema.TypeString, + Computed: true, + }, + + "user_data_name": { + Type: schema.TypeString, + Computed: true, + }, + + "user_data_params": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func validateUserDataPolicy(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + validPolicies := []string{"ALLOWOVERRIDE", "APPEND", "DENYOVERRIDE"} + + for _, policy := range validPolicies { + if value == policy { + return + } + } + + errors = append(errors, fmt.Errorf("user_data_policy must be one of: %v", validPolicies)) + return +} + +func resourceCloudStackUserDataTemplateLinkCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create parameter struct + p := cs.Template.NewLinkUserDataToTemplateParams() + + // Set template or ISO ID + if templateId, ok := d.GetOk("template_id"); ok { + p.SetTemplateid(templateId.(string)) + d.SetId(fmt.Sprintf("template-%s", templateId.(string))) + } else if isoId, ok := d.GetOk("iso_id"); ok { + p.SetIsoid(isoId.(string)) + d.SetId(fmt.Sprintf("iso-%s", isoId.(string))) + } else { + return fmt.Errorf("Either template_id or iso_id must be specified") + } + + // Set optional parameters + if userDataId, ok := d.GetOk("user_data_id"); ok { + p.SetUserdataid(userDataId.(string)) + } + + if userDataPolicy, ok := d.GetOk("user_data_policy"); ok { + p.SetUserdatapolicy(userDataPolicy.(string)) + } + + log.Printf("[DEBUG] Linking UserData to Template/ISO") + r, err := cs.Template.LinkUserDataToTemplate(p) + if err != nil { + return fmt.Errorf("Error linking UserData to Template/ISO: %s", err) + } + + // Store the template/ISO ID as resource ID + if r.Id != "" { + d.SetId(r.Id) + } + + return resourceCloudStackUserDataTemplateLinkRead(d, meta) +} + +func resourceCloudStackUserDataTemplateLinkRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + var template *cloudstack.Template + var err error + var count int + + // Determine if we're dealing with a template or ISO + if templateId, ok := d.GetOk("template_id"); ok { + // Get template details + template, count, err = cs.Template.GetTemplateByID( + templateId.(string), + "all", + ) + } else if isoId, ok := d.GetOk("iso_id"); ok { + // Get ISO details (ISOs are also handled by the Template service) + template, count, err = cs.Template.GetTemplateByID( + isoId.(string), + "all", + ) + } else { + return fmt.Errorf("Either template_id or iso_id must be specified") + } + + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Template/ISO no longer exists") + d.SetId("") + return nil + } + return err + } + + // Update computed attributes + d.Set("name", template.Name) + d.Set("display_text", template.Displaytext) + d.Set("is_ready", template.Isready) + d.Set("template_type", template.Templatetype) + d.Set("user_data_name", template.Userdataname) + d.Set("user_data_params", template.Userdataparams) + + return nil +} + +func resourceCloudStackUserDataTemplateLinkUpdate(d *schema.ResourceData, meta interface{}) error { + // If user_data_id or user_data_policy changes, we need to re-link + if d.HasChange("user_data_id") || d.HasChange("user_data_policy") { + return resourceCloudStackUserDataTemplateLinkCreate(d, meta) + } + + return resourceCloudStackUserDataTemplateLinkRead(d, meta) +} + +func resourceCloudStackUserDataTemplateLinkDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create parameter struct for unlinking (no userdata id = unlink) + p := cs.Template.NewLinkUserDataToTemplateParams() + + // Set template or ISO ID + if templateId, ok := d.GetOk("template_id"); ok { + p.SetTemplateid(templateId.(string)) + } else if isoId, ok := d.GetOk("iso_id"); ok { + p.SetIsoid(isoId.(string)) + } else { + return fmt.Errorf("Either template_id or iso_id must be specified") + } + + // Don't set userdataid - this will unlink existing userdata + + log.Printf("[DEBUG] Unlinking UserData from Template/ISO") + _, err := cs.Template.LinkUserDataToTemplate(p) + if err != nil { + return fmt.Errorf("Error unlinking UserData from Template/ISO: %s", err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_user_data_template_link_test.go b/cloudstack/resource_cloudstack_user_data_template_link_test.go new file mode 100644 index 00000000..e921d78a --- /dev/null +++ b/cloudstack/resource_cloudstack_user_data_template_link_test.go @@ -0,0 +1,195 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackUserDataTemplateLink_template(t *testing.T) { + var template cloudstack.Template + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackUserDataTemplateLinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackUserDataTemplateLinkConfigTemplate, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackTemplateExists("cloudstack_template.test", &template), + testAccCheckCloudStackUserDataTemplateLinkExists("cloudstack_user_data_template_link.test"), + resource.TestCheckResourceAttr("cloudstack_user_data_template_link.test", "user_data_policy", "ALLOWOVERRIDE"), + ), + }, + }, + }) +} + +func TestAccCloudStackUserDataTemplateLink_iso(t *testing.T) { + var template cloudstack.Template + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackUserDataTemplateLinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackUserDataTemplateLinkConfigISO, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackTemplateExists("cloudstack_template.test_iso", &template), + testAccCheckCloudStackUserDataTemplateLinkExists("cloudstack_user_data_template_link.test_iso"), + resource.TestCheckResourceAttr("cloudstack_user_data_template_link.test_iso", "user_data_policy", "APPEND"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackUserDataTemplateLinkExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No UserData Template Link ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + // Check if template/ISO exists with linked userdata + var templateId string + if rs.Primary.Attributes["template_id"] != "" { + templateId = rs.Primary.Attributes["template_id"] + } else if rs.Primary.Attributes["iso_id"] != "" { + templateId = rs.Primary.Attributes["iso_id"] + } else { + return fmt.Errorf("Neither template_id nor iso_id found in state") + } + + template, count, err := cs.Template.GetTemplateByID(templateId, "all") + if err != nil { + if count == 0 { + return fmt.Errorf("Template/ISO %s not found", templateId) + } + return err + } + + // Check if userdata is linked (optional since unlinking is also valid) + if template.Userdataname != "" { + return nil // UserData is linked + } + + return nil // Template exists, whether userdata is linked or not + } +} + +func testAccCheckCloudStackUserDataTemplateLinkDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_user_data_template_link" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No UserData Template Link ID is set") + } + + var templateId string + if rs.Primary.Attributes["template_id"] != "" { + templateId = rs.Primary.Attributes["template_id"] + } else if rs.Primary.Attributes["iso_id"] != "" { + templateId = rs.Primary.Attributes["iso_id"] + } else { + continue // Skip if no template/ISO ID + } + + template, count, err := cs.Template.GetTemplateByID(templateId, "all") + if err != nil { + if count == 0 { + return nil // Template doesn't exist anymore, that's fine + } + return err + } + + // Check that userdata is not linked anymore + if template.Userdataname != "" { + return fmt.Errorf("UserData is still linked to template/ISO %s", templateId) + } + } + + return nil +} + +const testAccCloudStackUserDataTemplateLinkConfigTemplate = ` +resource "cloudstack_user_data" "test" { + name = "test-userdata-link" + user_data = "#!/bin/bash\necho 'template test' > /tmp/test.txt" +} + +resource "cloudstack_template" "test" { + name = "test-template-userdata" + format = "QCOW2" + hypervisor = "KVM" + os_type = "CentOS 7" + url = "http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img" + is_extractable = true + is_featured = false + is_public = false + password_enabled = false +} + +resource "cloudstack_user_data_template_link" "test" { + template_id = cloudstack_template.test.id + user_data_id = cloudstack_user_data.test.id + user_data_policy = "ALLOWOVERRIDE" +}` + +const testAccCloudStackUserDataTemplateLinkConfigISO = ` +resource "cloudstack_user_data" "test_iso" { + name = "test-userdata-iso-link" + user_data = "#!/bin/bash\necho 'iso test' > /tmp/test.txt" +} + +resource "cloudstack_template" "test_iso" { + name = "test-iso-userdata" + format = "ISO" + hypervisor = "KVM" + os_type = "CentOS 7" + url = "http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img" + is_extractable = true + is_featured = false + is_public = false + password_enabled = false +} + +resource "cloudstack_user_data_template_link" "test_iso" { + iso_id = cloudstack_template.test_iso.id + user_data_id = cloudstack_user_data.test_iso.id + user_data_policy = "APPEND" +}` diff --git a/cloudstack/resource_cloudstack_user_data_test.go b/cloudstack/resource_cloudstack_user_data_test.go new file mode 100644 index 00000000..1da85851 --- /dev/null +++ b/cloudstack/resource_cloudstack_user_data_test.go @@ -0,0 +1,141 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackUserData_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackUserDataDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackUserData_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackUserDataExists("cloudstack_user_data.foobar"), + resource.TestCheckResourceAttr("cloudstack_user_data.foobar", "name", "terraform-test-userdata"), + resource.TestCheckResourceAttr("cloudstack_user_data.foobar", "user_data", "#!/bin/bash\necho 'Hello World'\n"), + ), + }, + }, + }) +} + +func TestAccCloudStackUserData_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackUserDataDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackUserData_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackUserDataExists("cloudstack_user_data.foobar"), + resource.TestCheckResourceAttr("cloudstack_user_data.foobar", "name", "terraform-test-userdata"), + resource.TestCheckResourceAttr("cloudstack_user_data.foobar", "user_data", "#!/bin/bash\necho 'Hello World'\n"), + ), + }, + { + Config: testAccCloudStackUserData_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackUserDataExists("cloudstack_user_data.foobar"), + resource.TestCheckResourceAttr("cloudstack_user_data.foobar", "name", "terraform-test-userdata"), + resource.TestCheckResourceAttr("cloudstack_user_data.foobar", "user_data", "#!/bin/bash\necho 'Updated Hello World'\n"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackUserDataExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No UserData ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + _, count, err := cs.User.GetUserDataByID(rs.Primary.ID) + + if err != nil { + return err + } + + if count == 0 { + return fmt.Errorf("UserData not found") + } + + return nil + } +} + +func testAccCheckCloudStackUserDataDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_user_data" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No UserData ID is set") + } + + _, count, err := cs.User.GetUserDataByID(rs.Primary.ID) + + if err == nil && count != 0 { + return fmt.Errorf("UserData %s still exists", rs.Primary.ID) + } + } + + return nil +} + +const testAccCloudStackUserData_basic = ` +resource "cloudstack_user_data" "foobar" { + name = "terraform-test-userdata" + user_data = <<-EOF + #!/bin/bash + echo 'Hello World' + EOF +} +` + +const testAccCloudStackUserData_update = ` +resource "cloudstack_user_data" "foobar" { + name = "terraform-test-userdata" + user_data = <<-EOF + #!/bin/bash + echo 'Updated Hello World' + EOF +} +` diff --git a/website/docs/d/user_data.html.markdown b/website/docs/d/user_data.html.markdown new file mode 100644 index 00000000..6ee845e8 --- /dev/null +++ b/website/docs/d/user_data.html.markdown @@ -0,0 +1,49 @@ +--- +layout: "cloudstack" +page_title: "Cloudstack: cloudstack_user_data" +sidebar_current: "docs-cloudstack-cloudstack_user_data" +description: |- + Retrieves information about an existing CloudStack UserData definition. +--- + +# cloudstack_user_data + +Use this data source to look up a registered CloudStack UserData definition so that its contents can be re-used across resources. + +## Example Usage + +```hcl +data "cloudstack_user_data" "cloudinit" { + name = "bootstrap-userdata" + + # Optional filters + account = "devops" + project = "automation" +} + +resource "cloudstack_instance" "vm" { + # ... other arguments ... + user_data = data.cloudstack_user_data.cloudinit.user_data +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` – (Required) The name of the UserData definition to retrieve. +* `account` – (Optional) Limits the lookup to a specific account. +* `project` – (Optional) Limits the lookup to a specific project. You may supply either the project name or ID. + +## Attributes Reference + +In addition to the arguments above, the following attributes are exported: + +* `id` – The ID of the UserData definition. +* `account` – The owning account name. +* `account_id` – The owning account ID. +* `domain` – The domain name in which the UserData resides. +* `domain_id` – The domain ID in which the UserData resides. +* `params` – The optional parameters string associated with the UserData. +* `project` – The project name, when applicable. +* `user_data` – The decoded UserData contents. diff --git a/website/docs/r/user_data.html.markdown b/website/docs/r/user_data.html.markdown new file mode 100644 index 00000000..fb83e746 --- /dev/null +++ b/website/docs/r/user_data.html.markdown @@ -0,0 +1,62 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_user_data" +sidebar_current: "docs-cloudstack-resource-user-data" +description: |- + Manages reusable user data scripts that can be linked to templates or instances. +--- + +# cloudstack_user_data + +Registers a reusable piece of user data in CloudStack. The stored script can be +linked to templates or referenced by instances that support user data. + +## Example Usage + +```hcl +resource "cloudstack_user_data" "bootstrap" { + name = "bootstrap-script" + user_data = <<-EOF + #!/bin/bash + echo "Hello from Terraform" > /var/tmp/hello.txt + EOF + + params = "key=value" + project = "infra-project" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The display name for the user data object. + +* `user_data` - (Required) The script or payload to store. The provider handles + Base64 encoding and validates the CloudStack size limit (1 MB encoded). + +* `account` - (Optional) The name of the account that owns the user data. Changing + this forces a new resource to be created. + +* `project` - (Optional) The name or ID of the project that owns the user data. + Changing this forces a new resource to be created. + +* `params` - (Optional) Additional parameters that will be passed to the user + data when it is executed. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the user data object. +* `account_id` - The ID of the owning account. +* `domain` - The name of the domain that owns the user data. +* `domain_id` - The ID of the domain that owns the user data. + +## Import + +User data can be imported using the `id`, e.g. + +```shell +terraform import cloudstack_user_data.bootstrap 2c1bab14-5fcb-4b52-bdba-2f7d4a4fb916 +``` diff --git a/website/docs/r/user_data_template_link.html.markdown b/website/docs/r/user_data_template_link.html.markdown new file mode 100644 index 00000000..8e81ad68 --- /dev/null +++ b/website/docs/r/user_data_template_link.html.markdown @@ -0,0 +1,88 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_user_data_template_link" +sidebar_current: "docs-cloudstack-resource-user-data-template-link" +description: |- + Attaches an existing user data object to a template or ISO. +--- + +# cloudstack_user_data_template_link + +Manages the association between a CloudStack template (or ISO) and a stored +user data object. Linking user data allows VMs created from the template to +receive the script automatically. + +## Example Usage + +### Link user data to a template + +```hcl +resource "cloudstack_user_data" "bootstrap" { + name = "bootstrap" + user_data = "#!/bin/bash\necho bootstrap > /var/tmp/bootstrap.log" +} + +resource "cloudstack_template" "base" { + name = "base-template" + format = "QCOW2" + hypervisor = "KVM" + os_type = "Ubuntu 22.04 (64-bit)" + url = "http://example.com/images/ubuntu-2204.qcow2" +} + +resource "cloudstack_user_data_template_link" "base_userdata" { + template_id = cloudstack_template.base.id + user_data_id = cloudstack_user_data.bootstrap.id + user_data_policy = "ALLOWOVERRIDE" +} +``` + +### Link user data to an ISO + +```hcl +resource "cloudstack_user_data_template_link" "iso_userdata" { + iso_id = cloudstack_template.iso_image.id + user_data_id = cloudstack_user_data.bootstrap.id + user_data_policy = "APPEND" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `template_id` - (Optional) The ID of the template to link. Conflicts with + `iso_id`. One of `template_id` or `iso_id` must be supplied. Changing the + target template forces a new resource to be created. + +* `iso_id` - (Optional) The ID of the ISO to link. Conflicts with `template_id`. + Changing the target ISO forces a new resource to be created. + +* `user_data_id` - (Optional) The ID of the user data object to associate. When + omitted, the resource only ensures the template is present and can be used to + remove an existing link via deletion. + +* `user_data_policy` - (Optional) The policy that defines how the linked user + data interacts with any user data provided during VM deployment. Valid values + are `ALLOWOVERRIDE`, `APPEND`, and `DENYOVERRIDE`. Defaults to `ALLOWOVERRIDE`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The template or ISO ID reported by CloudStack. +* `name` - The name of the template or ISO. +* `display_text` - The display text of the template or ISO. +* `is_ready` - Whether the template or ISO is ready for use. +* `template_type` - The CloudStack template type. +* `user_data_name` - The name of the linked user data object, if any. +* `user_data_params` - The parameters stored with the linked user data. + +## Import + +User data template links can be imported using the CloudStack identifier of the +linked template or ISO, e.g. + +```shell +terraform import cloudstack_user_data_template_link.base_userdata template-8a7d5c64-0605-4ed6-b3a2-7c91c7f5d4bb +```