diff --git a/cloudstack/data_source_cloudstack_user_data.go b/cloudstack/data_source_cloudstack_user_data.go new file mode 100644 index 00000000..29ef5019 --- /dev/null +++ b/cloudstack/data_source_cloudstack_user_data.go @@ -0,0 +1,128 @@ +// +// 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 dataSourceCloudstackUserData() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudstackUserDataRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + "account": { + Type: schema.TypeString, + Computed: true, + }, + "account_id": { + Type: schema.TypeString, + Computed: true, + }, + "domain": { + Type: schema.TypeString, + Computed: true, + }, + "domain_id": { + Type: schema.TypeString, + Computed: true, + }, + "project": { + Type: schema.TypeString, + Computed: true, + }, + "project_id": { + Type: schema.TypeString, + Computed: true, + }, + "userdata_id": { + Type: schema.TypeString, + Computed: true, + }, + "userdata": { + Type: schema.TypeString, + Computed: true, + }, + "params": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceCloudstackUserDataRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + p := cs.User.NewListUserDataParams() + p.SetName(name) + + if v, ok := d.GetOk("account"); ok { + p.SetAccount(v.(string)) + } + if v, ok := d.GetOk("domain_id"); ok { + p.SetDomainid(v.(string)) + } + + log.Printf("[DEBUG] Listing user data with name: %s", name) + userdataList, err := cs.User.ListUserData(p) + if err != nil { + return fmt.Errorf("Error listing user data with name %s: %s", name, err) + } + + if len(userdataList.UserData) == 0 { + return fmt.Errorf("No user data found with name: %s", name) + } + if len(userdataList.UserData) > 1 { + return fmt.Errorf("Multiple user data entries found with name: %s", name) + } + + userdata := userdataList.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("userdata_id", userdata.Id) + d.Set("params", userdata.Params) + + if userdata.Project != "" { + d.Set("project", userdata.Project) + d.Set("project_id", userdata.Projectid) + } + + if userdata.Userdata != "" { + decoded, err := base64.StdEncoding.DecodeString(userdata.Userdata) + if err != nil { + d.Set("userdata", userdata.Userdata) // Fallback: use raw data + } else { + d.Set("userdata", string(decoded)) + } + } + return nil +} diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 63d02349..72090147 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -104,6 +104,7 @@ func Provider() *schema.Provider { "cloudstack_quota": dataSourceCloudStackQuota(), "cloudstack_quota_enabled": dataSourceCloudStackQuotaEnabled(), "cloudstack_quota_tariff": dataSourceCloudStackQuotaTariff(), + "cloudstack_user_data": dataSourceCloudstackUserData(), }, ResourcesMap: map[string]*schema.Resource{ @@ -164,6 +165,7 @@ func Provider() *schema.Provider { "cloudstack_limits": resourceCloudStackLimits(), "cloudstack_snapshot_policy": resourceCloudStackSnapshotPolicy(), "cloudstack_quota_tariff": resourceCloudStackQuotaTariff(), + "cloudstack_user_data": resourceCloudStackUserData(), }, ConfigureFunc: providerConfigure, diff --git a/cloudstack/resource_cloudstack_instance.go b/cloudstack/resource_cloudstack_instance.go index 4caa345d..6a38ddb4 100644 --- a/cloudstack/resource_cloudstack_instance.go +++ b/cloudstack/resource_cloudstack_instance.go @@ -212,6 +212,17 @@ func resourceCloudStackInstance() *schema.Resource { }, }, + "userdata_id": { + Type: schema.TypeString, + Optional: true, + }, + + "userdata_details": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "details": { Type: schema.TypeMap, Optional: true, @@ -446,6 +457,20 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) p.SetUserdata(ud) } + if userdataID, ok := d.GetOk("userdata_id"); ok { + p.SetUserdataid(userdataID.(string)) + } + + if userdataDetails, ok := d.GetOk("userdata_details"); ok { + udDetails := make(map[string]string) + index := 0 + for k, v := range userdataDetails.(map[string]interface{}) { + udDetails[fmt.Sprintf("userdatadetails[%d].%s", index, k)] = v.(string) + index++ + } + p.SetUserdatadetails(udDetails) + } + // Create the new instance r, err := cs.VirtualMachine.DeployVirtualMachine(p) if err != nil { @@ -560,6 +585,23 @@ func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) er d.Set("boot_mode", vm.Bootmode) } + if vm.Userdataid != "" { + d.Set("userdata_id", vm.Userdataid) + } + + if vm.Userdata != "" { + decoded, err := base64.StdEncoding.DecodeString(vm.Userdata) + if err != nil { + d.Set("user_data", vm.Userdata) + } else { + d.Set("user_data", string(decoded)) + } + } + + if vm.Userdatadetails != "" { + log.Printf("[DEBUG] Instance %s has userdata details: %s", vm.Name, vm.Userdatadetails) + } + return nil } @@ -609,7 +651,8 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) // Attributes that require reboot to update if d.HasChange("name") || d.HasChange("service_offering") || d.HasChange("affinity_group_ids") || - d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("keypairs") || d.HasChange("user_data") { + d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("keypairs") || + d.HasChange("user_data") || d.HasChange("userdata_id") || d.HasChange("userdata_details") { // Before we can actually make these changes, the virtual machine must be stopped _, err := cs.VirtualMachine.StopVirtualMachine( @@ -763,6 +806,40 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + if d.HasChange("userdata_id") { + log.Printf("[DEBUG] userdata_id changed for %s, starting update", name) + + p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) + if userdataID, ok := d.GetOk("userdata_id"); ok { + p.SetUserdataid(userdataID.(string)) + } + _, err := cs.VirtualMachine.UpdateVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error updating userdata_id for instance %s: %s", name, err) + } + } + + if d.HasChange("userdata_details") { + log.Printf("[DEBUG] userdata_details changed for %s, starting update", name) + + p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) + if userdataDetails, ok := d.GetOk("userdata_details"); ok { + udDetails := make(map[string]string) + index := 0 + for k, v := range userdataDetails.(map[string]interface{}) { + udDetails[fmt.Sprintf("userdatadetails[%d].%s", index, k)] = v.(string) + index++ + } + p.SetUserdatadetails(udDetails) + } + _, err := cs.VirtualMachine.UpdateVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error updating userdata_details for instance %s: %s", name, err) + } + } + // Start the virtual machine again _, err = cs.VirtualMachine.StartVirtualMachine( cs.VirtualMachine.NewStartVirtualMachineParams(d.Id())) diff --git a/cloudstack/resource_cloudstack_template.go b/cloudstack/resource_cloudstack_template.go index 4316c7eb..ced400a5 100644 --- a/cloudstack/resource_cloudstack_template.go +++ b/cloudstack/resource_cloudstack_template.go @@ -133,6 +133,37 @@ func resourceCloudStackTemplate() *schema.Resource { ForceNew: true, }, + "userdata_link": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "userdata_id": { + Type: schema.TypeString, + Required: true, + Description: "The ID of the user data to link to the template.", + }, + "userdata_policy": { + Type: schema.TypeString, + Optional: true, + Default: "ALLOWOVERRIDE", + Description: "Override policy of the userdata. Possible values: ALLOWOVERRIDE, APPEND, DENYOVERRIDE. Default: ALLOWOVERRIDE", + }, + "userdata_name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the linked user data.", + }, + "userdata_params": { + Type: schema.TypeString, + Computed: true, + Description: "The parameters of the linked user data.", + }, + }, + }, + }, + "tags": tagsSchema(), }, } @@ -224,6 +255,11 @@ func resourceCloudStackTemplateCreate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Error setting tags on the template %s: %s", name, err) } + // Link userdata if specified + if err = linkUserdataToTemplate(cs, d, r.RegisterTemplate[0].Id); err != nil { + return fmt.Errorf("Error linking userdata to template %s: %s", name, err) + } + // Wait until the template is ready to use, or timeout with an error... currentTime := time.Now().Unix() timeout := int64(d.Get("is_ready_timeout").(int)) @@ -300,6 +336,11 @@ func resourceCloudStackTemplateRead(d *schema.ResourceData, meta interface{}) er setValueOrID(d, "project", t.Project, t.Projectid) setValueOrID(d, "zone", t.Zonename, t.Zoneid) + // Read userdata link information + if err := readUserdataFromTemplate(d, t); err != nil { + return fmt.Errorf("Error reading userdata link from template: %s", err) + } + return nil } @@ -349,6 +390,12 @@ func resourceCloudStackTemplateUpdate(d *schema.ResourceData, meta interface{}) } } + if d.HasChange("userdata_link") { + if err := updateUserdataLink(cs, d); err != nil { + return fmt.Errorf("Error updating userdata link for template %s: %s", name, err) + } + } + return resourceCloudStackTemplateRead(d, meta) } @@ -383,3 +430,105 @@ func verifyTemplateParams(d *schema.ResourceData) error { return nil } + +func linkUserdataToTemplate(cs *cloudstack.CloudStackClient, d *schema.ResourceData, templateID string) error { + userdataLinks := d.Get("userdata_link").([]interface{}) + if len(userdataLinks) == 0 { + return nil + } + + userdataLink := userdataLinks[0].(map[string]interface{}) + + p := cs.Template.NewLinkUserDataToTemplateParams() + p.SetTemplateid(templateID) + p.SetUserdataid(userdataLink["userdata_id"].(string)) + + if policy, ok := userdataLink["userdata_policy"].(string); ok && policy != "" { + p.SetUserdatapolicy(policy) + } + + _, err := cs.Template.LinkUserDataToTemplate(p) + return err +} + +func readUserdataFromTemplate(d *schema.ResourceData, template *cloudstack.Template) error { + if template.Userdataid == "" { + d.Set("userdata_link", []interface{}{}) + return nil + } + + userdataLink := map[string]interface{}{ + "userdata_id": template.Userdataid, + "userdata_name": template.Userdataname, + "userdata_params": template.Userdataparams, + } + + if existingLinks := d.Get("userdata_link").([]interface{}); len(existingLinks) > 0 { + if existingLink, ok := existingLinks[0].(map[string]interface{}); ok { + if policy, exists := existingLink["userdata_policy"]; exists { + userdataLink["userdata_policy"] = policy + } + } + } + + d.Set("userdata_link", []interface{}{userdataLink}) + return nil +} + +func updateUserdataLink(cs *cloudstack.CloudStackClient, d *schema.ResourceData) error { + templateID := d.Id() + + oldLinks, newLinks := d.GetChange("userdata_link") + oldLinksSlice := oldLinks.([]interface{}) + newLinksSlice := newLinks.([]interface{}) + + // Check if we're removing userdata link (had one before, now empty) + if len(oldLinksSlice) > 0 && len(newLinksSlice) == 0 { + unlinkP := cs.Template.NewLinkUserDataToTemplateParams() + unlinkP.SetTemplateid(templateID) + + _, err := cs.Template.LinkUserDataToTemplate(unlinkP) + if err != nil { + return fmt.Errorf("Error unlinking userdata from template: %s", err) + } + log.Printf("[DEBUG] Unlinked userdata from template: %s", templateID) + return nil + } + + if len(newLinksSlice) > 0 { + newLink := newLinksSlice[0].(map[string]interface{}) + + if len(oldLinksSlice) > 0 { + oldLink := oldLinksSlice[0].(map[string]interface{}) + + if oldLink["userdata_id"].(string) == newLink["userdata_id"].(string) { + oldPolicy := "" + newPolicy := "" + + if p, ok := oldLink["userdata_policy"].(string); ok { + oldPolicy = p + } + if p, ok := newLink["userdata_policy"].(string); ok { + newPolicy = p + } + + if oldPolicy == newPolicy { + log.Printf("[DEBUG] Userdata link unchanged, skipping API call") + return nil + } + } + + unlinkP := cs.Template.NewLinkUserDataToTemplateParams() + unlinkP.SetTemplateid(templateID) + + _, err := cs.Template.LinkUserDataToTemplate(unlinkP) + if err != nil { + log.Printf("[DEBUG] Error unlinking existing userdata (this may be normal): %s", err) + } + } + + return linkUserdataToTemplate(cs, d, templateID) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_user_data.go b/cloudstack/resource_cloudstack_user_data.go new file mode 100644 index 00000000..65a3d2ca --- /dev/null +++ b/cloudstack/resource_cloudstack_user_data.go @@ -0,0 +1,166 @@ +// +// 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" + "strings" + + "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, + Delete: resourceCloudStackUserDataDelete, + Importer: &schema.ResourceImporter{ + State: importStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the user data", + }, + + "userdata": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The user data content to be registered", + }, + + "account": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional account for the user data. Must be used with domain_id.", + }, + + "domain_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional domain ID for the user data. If the account parameter is used, domain_id must also be used.", + }, + + "params": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Description: "Optional comma separated list of variables declared in user data content.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "project_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional project for the user data.", + }, + }, + } +} + +func resourceCloudStackUserDataCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.User.NewRegisterUserDataParams(d.Get("name").(string), d.Get("userdata").(string)) + if v, ok := d.GetOk("account"); ok { + p.SetAccount(v.(string)) + } + if v, ok := d.GetOk("domain_id"); ok { + p.SetDomainid(v.(string)) + } + if v, ok := d.GetOk("project_id"); ok { + p.SetProjectid(v.(string)) + } + if v, ok := d.GetOk("params"); ok { + paramsList := v.(*schema.Set).List() + var params []string + for _, param := range paramsList { + params = append(params, param.(string)) + } + p.SetParams(strings.Join(params, ",")) + } + + userdata, err := cs.User.RegisterUserData(p) + if err != nil { + return fmt.Errorf("Error registering user data: %s", err) + } + + d.SetId(userdata.Id) + + return resourceCloudStackUserDataRead(d, meta) +} + +func resourceCloudStackUserDataRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + id := d.Id() + + p := cs.User.NewListUserDataParams() + p.SetId(id) + + userdata, err := cs.User.ListUserData(p) + if err != nil { + return fmt.Errorf("Error retrieving user data with ID %s: %s", id, err) + } + + d.Set("name", userdata.UserData[0].Name) + d.Set("userdata", userdata.UserData[0].Userdata) + if d.Get("account").(string) != "" { + d.Set("account", userdata.UserData[0].Account) + } + if d.Get("domain_id").(string) != "" { + d.Set("domain_id", userdata.UserData[0].Domainid) + } + if userdata.UserData[0].Params != "" { + paramsList := strings.Split(userdata.UserData[0].Params, ",") + var paramsSet []interface{} + for _, param := range paramsList { + paramsSet = append(paramsSet, param) + } + d.Set("params", schema.NewSet(schema.HashString, paramsSet)) + } + if userdata.UserData[0].Projectid != "" { + d.Set("project_id", userdata.UserData[0].Projectid) + } + + return nil +} + +func resourceCloudStackUserDataDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.User.NewDeleteUserDataParams(d.Id()) + _, err := cs.User.DeleteUserData(p) + if err != nil { + return fmt.Errorf("Error deleting user data with ID %s: %s", d.Id(), err) + } + + return nil +} diff --git a/website/docs/README.md b/website/docs/README.md index 0c09b532..2fe8dd3f 100644 --- a/website/docs/README.md +++ b/website/docs/README.md @@ -66,6 +66,7 @@ The following arguments are supported: - [ssh_keypair](./d/ssh_keypair.html.markdown) - [template](./d/template.html.markdown) - [user](./d/user.html.markdown) +- [user_data](./d/user_data.html.markdown) - [volume](./d/volume.html.markdown) - [vpc](./d/vpc.html.markdown) - [vpn_connection](./d/vpn_connection.html.markdown) @@ -101,6 +102,7 @@ The following arguments are supported: - [static_route](./r/static_route.html.markdown) - [template](./r/template.html.markdown) - [user](./r/user.html.markdown) +- [userdata](./r/userdata.html.markdown) - [volume](./r/volume.html.markdown) - [vpc](./r/vpc.html.markdown) - [vpn_connection](./r/vpn_connection.html.markdown) diff --git a/website/docs/d/user_data.html.markdown b/website/docs/d/user_data.html.markdown new file mode 100644 index 00000000..1e431324 --- /dev/null +++ b/website/docs/d/user_data.html.markdown @@ -0,0 +1,126 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_user_data" +sidebar_current: "docs-cloudstack-datasource-user-data" +description: |- + Get information about a CloudStack user data. +--- + +# cloudstack_user_data + +Use this data source to retrieve information about a CloudStack user data by either its name or ID. + +## Example Usage + +### Find User Data by Name + +```hcl +data "cloudstack_user_data" "web_init" { + filter { + name = "name" + value = "web-server-init" + } +} + +# Use the user data in an instance +resource "cloudstack_instance" "web" { + name = "web-server" + userdata_id = data.cloudstack_user_data.web_init.id + # ... other arguments ... +} +``` + +### Find User Data by ID + +```hcl +data "cloudstack_user_data" "app_init" { + filter { + name = "id" + value = "12345678-1234-1234-1234-123456789012" + } +} +``` + +### Find Project-Scoped User Data + +```hcl +data "cloudstack_user_data" "project_init" { + project = "my-project" + + filter { + name = "name" + value = "project-specific-init" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `filter` - (Required) One or more name/value pairs to filter off of. You can apply multiple filters to narrow down the results. See [Filters](#filters) below for more details. + +* `project` - (Optional) The name or ID of the project to search in. + +### Filters + +The `filter` block supports the following arguments: + +* `name` - (Required) The name of the filter. Valid filter names are: + * `id` - Filter by user data ID + * `name` - Filter by user data name + * `account` - Filter by account name + * `domainid` - Filter by domain ID + +* `value` - (Required) The value to filter by. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The user data ID. +* `name` - The name of the user data. +* `userdata` - The user data content. +* `account` - The account name owning the user data. +* `domain_id` - The domain ID where the user data belongs. +* `project_id` - The project ID if the user data is project-scoped. +* `params` - The list of parameter names defined in the user data (comma-separated string). + +## Example with Template Integration + +```hcl +# Find existing user data +data "cloudstack_user_data" "app_bootstrap" { + filter { + name = "name" + value = "application-bootstrap" + } +} + +# Use with template +resource "cloudstack_template" "app_template" { + name = "application-template" + display_text = "Application Template with Bootstrap" + # ... other template arguments ... + + userdata_link { + userdata_id = data.cloudstack_user_data.app_bootstrap.id + userdata_policy = "ALLOWOVERRIDE" + } +} + +# Deploy instance with parameterized user data +resource "cloudstack_instance" "app_server" { + name = "app-server-01" + template = cloudstack_template.app_template.id + # ... other instance arguments ... + + userdata_id = data.cloudstack_user_data.app_bootstrap.id + + userdata_details = { + "environment" = "production" + "app_version" = "v2.1.0" + "debug_enabled" = "false" + } +} +``` diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index 7b0b1bdd..ebf3f20f 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -13,6 +13,8 @@ disk offering, and template. ## Example Usage +### Basic Instance + ```hcl resource "cloudstack_instance" "web" { name = "server-1" @@ -23,6 +25,86 @@ resource "cloudstack_instance" "web" { } ``` +### Instance with Inline User Data + +```hcl +resource "cloudstack_instance" "web_with_userdata" { + name = "web-server" + service_offering = "small" + network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" + template = "Ubuntu 20.04" + zone = "zone-1" + + user_data = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + systemctl enable nginx + systemctl start nginx + EOF + ) +} +``` + +### Instance with Registered User Data + +```hcl +# First, create registered user data +resource "cloudstack_userdata" "web_init" { + name = "web-server-init" + + userdata = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + + # Use parameters + echo "
Environment: $${environment}
" >> /var/www/html/index.html + + systemctl enable nginx + systemctl start nginx + EOF + ) + + params = ["app_name", "environment"] +} + +# Deploy instance with parameterized user data +resource "cloudstack_instance" "app_server" { + name = "app-server-01" + service_offering = "medium" + network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" + template = "Ubuntu 20.04" + zone = "zone-1" + + userdata_id = cloudstack_userdata.web_init.id + + userdata_details = { + "app_name" = "My Application" + "environment" = "production" + } +} +``` + +### Instance with Template-Linked User Data + +```hcl +# Use a template that has user data pre-linked +resource "cloudstack_instance" "from_template" { + name = "template-instance" + service_offering = "small" + network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" + template = cloudstack_template.web_template.id # Template with userdata_link + zone = "zone-1" + + # Override parameters for the template's linked user data + userdata_details = { + "app_name" = "Template-Based App" + } +} +``` + ## Argument Reference The following arguments are supported: @@ -93,6 +175,10 @@ The following arguments are supported: * `user_data` - (Optional) The user data to provide when launching the instance. This can be either plain text or base64 encoded text. +* `userdata_id` - (Optional) The ID of a registered CloudStack user data to use for this instance. Cannot be used together with `user_data`. + +* `userdata_details` - (Optional) A map of key-value pairs to pass as parameters to the user data script. Only valid when `userdata_id` is specified. Keys must match the parameter names defined in the user data. + * `keypair` - (Optional) The name of the SSH key pair that will be used to access this instance. (Mutual exclusive with keypairs) diff --git a/website/docs/r/template.html.markdown b/website/docs/r/template.html.markdown index 52eb7fa3..1f7a7d18 100644 --- a/website/docs/r/template.html.markdown +++ b/website/docs/r/template.html.markdown @@ -82,6 +82,10 @@ The following arguments are supported: * `for_cks` - (Optional) Set to `true` to indicate this template is for CloudStack Kubernetes Service (CKS). CKS templates have special requirements and capabilities. Defaults to `false`. +### User Data Integration + +* `userdata_link` - (Optional) Link user data to this template. When specified, instances deployed from this template will inherit the linked user data. See [User Data Link](#user-data-link) below for more details. + ### Template Properties * `is_dynamically_scalable` - (Optional) Set to indicate if the template contains tools to support dynamic scaling of VM cpu/memory. Defaults to `false`. @@ -136,6 +140,70 @@ The following attributes are exported: * `domain` - The domain name where the template belongs. * `project` - The project name if the template is assigned to a project. +## User Data Link + +The `userdata_link` block supports the following arguments: + +* `userdata_id` - (Required) The ID of the user data to link to this template. +* `userdata_policy` - (Required) The user data policy for instances deployed from this template. Valid values: + * `ALLOWOVERRIDE` - Allow instances to override the linked user data with their own + * `APPEND` - Append instance-specific user data to the template's linked user data + * `DENYOVERRIDE` - Prevent instances from overriding the linked user data + +When a `userdata_link` is configured, the following additional attributes are exported: + +* `userdata_name` - The name of the linked user data +* `userdata_params` - The parameters defined in the linked user data + +### Example Template with User Data + +```hcl +# Create user data +resource "cloudstack_userdata" "web_init" { + name = "web-server-initialization" + + userdata = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + echo "Environment: $${environment}
" >> /var/www/html/index.html + echo "Debug Mode: $${debug_mode}
" >> /var/www/html/index.html + + systemctl enable nginx + systemctl start nginx + EOF + ) + + # Define parameters that can be passed during instance deployment + params = ["app_name", "environment", "debug_mode"] +} +``` + +### Project-Scoped User Data + +```hcl +resource "cloudstack_user_data" "project_init" { + name = "project-specific-init" + project_id = "12345678-1234-1234-1234-123456789012" + + userdata = base64encode(<<-EOF + #!/bin/bash + # Project-specific initialization + echo "Initializing project environment..." + EOF + ) +} +``` + +## Argument Reference + +The following arguments are supported: + +### Required Arguments + +* `name` - (Required) The name of the user data. Must be unique within the account/project scope. +* `userdata` - (Required) The user data content to be registered. Should be base64 encoded. This is typically a cloud-init script or other initialization data. + +### Optional Arguments + +* `account` - (Optional) The account name for the user data. Must be used together with `domain_id`. If not specified, uses the current account. +* `domain_id` - (Optional) The domain ID for the user data. Required when `account` is specified. +* `project_id` - (Optional) The project ID to create this user data for. Cannot be used together with `account`/`domain_id`. +* `params` - (Optional) A list of parameter names that are declared in the user data content. These parameters can be passed values during instance deployment using `userdata_details`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The user data ID. +* `name` - The name of the user data. +* `userdata` - The registered user data content. +* `account` - The account name owning the user data. +* `domain_id` - The domain ID where the user data belongs. +* `project_id` - The project ID if the user data is project-scoped. +* `params` - The list of parameter names defined in the user data. + +## Usage with Templates and Instances + +User data can be used in multiple ways: + +### 1. Linked to Templates + +```hcl +resource "cloudstack_template" "web_template" { + name = "web-server-template" + # ... other template arguments ... + + userdata_link { + userdata_id = cloudstack_user_data.app_init.id + userdata_policy = "ALLOWOVERRIDE" # Allow instance to override + } +} +``` + +### 2. Direct Instance Usage + +```hcl +resource "cloudstack_instance" "web_server" { + name = "web-server-01" + # ... other instance arguments ... + + userdata_id = cloudstack_user_data.app_init.id # Pass parameter values to the userdata script + userdata_details = { + "app_name" = "My Web Application" + "environment" = "production" + "debug_mode" = "false" + } +} +``` + +## Import + +User data can be imported using the user data ID: + +``` +terraform import cloudstack_user_data.example 12345678-1234-1234-1234-123456789012 +``` + +## Notes + +* User data content should be base64 encoded before registration +* Parameter substitution in user data uses the format `${parameter_name}` +* Parameters must be declared in the `params` list to be usable +* User data is immutable after creation - changes require resource recreation +* Maximum user data size depends on CloudStack configuration (typically 32KB)