diff --git a/cloudstack/resource_cloudstack_account.go b/cloudstack/resource_cloudstack_account.go index db6c1a2c..1252398a 100644 --- a/cloudstack/resource_cloudstack_account.go +++ b/cloudstack/resource_cloudstack_account.go @@ -47,8 +47,9 @@ func resourceCloudStackAccount() *schema.Resource { Required: true, }, "password": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + Sensitive: true, }, "username": { Type: schema.TypeString, @@ -56,7 +57,8 @@ func resourceCloudStackAccount() *schema.Resource { }, "account_type": { Type: schema.TypeInt, - Required: true, + Optional: true, + Computed: true, }, "role_id": { Type: schema.TypeString, @@ -65,10 +67,12 @@ func resourceCloudStackAccount() *schema.Resource { "account": { Type: schema.TypeString, Optional: true, + Computed: true, }, "domain_id": { Type: schema.TypeString, Optional: true, + Computed: true, }, }, } @@ -82,20 +86,26 @@ func resourceCloudStackAccountCreate(d *schema.ResourceData, meta interface{}) e username := d.Get("username").(string) password := d.Get("password").(string) role_id := d.Get("role_id").(string) - account_type := d.Get("account_type").(int) account := d.Get("account").(string) domain_id := d.Get("domain_id").(string) // Create a new parameter struct p := cs.Account.NewCreateAccountParams(email, first_name, last_name, password, username) - p.SetAccounttype(int(account_type)) + // account_type is derived from the role when omitted; only send it when the + // user explicitly set it in config. GetRawConfig distinguishes an explicit + // 0 (a valid account type) from an omitted value, which GetOk cannot. + if cfg := d.GetRawConfig(); !cfg.IsNull() && !cfg.GetAttr("account_type").IsNull() { + p.SetAccounttype(d.Get("account_type").(int)) + } p.SetRoleid(role_id) if account != "" { p.SetAccount(account) } else { p.SetAccount(username) } - p.SetDomainid(domain_id) + if domain_id != "" { + p.SetDomainid(domain_id) + } log.Printf("[DEBUG] Creating Account %s", account) a, err := cs.Account.CreateAccount(p) @@ -110,9 +120,137 @@ func resourceCloudStackAccountCreate(d *schema.ResourceData, meta interface{}) e return resourceCloudStackAccountRead(d, meta) } -func resourceCloudStackAccountRead(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceCloudStackAccountRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Reading Account %s", d.Id()) + + p := cs.Account.NewListAccountsParams() + p.SetId(d.Id()) + + accounts, err := cs.Account.ListAccounts(p) + if err != nil { + return fmt.Errorf("Error retrieving Account %s: %s", d.Id(), err) + } + + if accounts.Count == 0 { + log.Printf("[DEBUG] Account %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + account := accounts.Accounts[0] + + d.Set("account_type", account.Accounttype) + d.Set("role_id", account.Roleid) + d.Set("account", account.Name) + d.Set("domain_id", account.Domainid) + + // Match the user this resource manages by username. CloudStack accounts can + // hold multiple users, so falling back to an arbitrary index could bind state + // to the wrong user; leave the user fields untouched when no match is found. + if user := accountUser(account, d.Get("username").(string)); user != nil { + d.Set("email", user.Email) + d.Set("first_name", user.Firstname) + d.Set("last_name", user.Lastname) + d.Set("username", user.Username) + } + + log.Printf("[DEBUG] Account %s successfully read", d.Id()) + return nil +} -func resourceCloudStackAccountUpdate(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceCloudStackAccountUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Updating Account %s", d.Id()) + + // Handle account-level changes + if d.HasChange("role_id") || d.HasChange("account") || d.HasChange("domain_id") { + p := cs.Account.NewUpdateAccountParams() + p.SetId(d.Id()) + + if d.HasChange("role_id") { + p.SetRoleid(d.Get("role_id").(string)) + } + if d.HasChange("account") { + if newName := d.Get("account").(string); newName != "" { + p.SetNewname(newName) + } + } + if d.HasChange("domain_id") { + p.SetDomainid(d.Get("domain_id").(string)) + } + + _, err := cs.Account.UpdateAccount(p) + if err != nil { + return fmt.Errorf("Error updating Account %s: %s", d.Id(), err) + } + } + + // Handle user-level changes via updateUser API + if d.HasChange("email") || d.HasChange("first_name") || d.HasChange("last_name") || d.HasChange("password") || d.HasChange("username") { + lp := cs.Account.NewListAccountsParams() + lp.SetId(d.Id()) + accounts, err := cs.Account.ListAccounts(lp) + if err != nil { + return fmt.Errorf("Error retrieving Account %s for user update: %s", d.Id(), err) + } + if accounts.Count == 0 { + return fmt.Errorf("Account %s no longer exists", d.Id()) + } + + // Match the user by the username we manage. On a username change the + // state still holds the previous username, so look it up by the old value. + oldUsername, _ := d.GetChange("username") + user := accountUser(accounts.Accounts[0], oldUsername.(string)) + if user == nil { + return fmt.Errorf("Account %s has no user %q to update", d.Id(), oldUsername) + } + + up := cs.User.NewUpdateUserParams(user.Id) + + if d.HasChange("email") { + up.SetEmail(d.Get("email").(string)) + } + if d.HasChange("first_name") { + up.SetFirstname(d.Get("first_name").(string)) + } + if d.HasChange("last_name") { + up.SetLastname(d.Get("last_name").(string)) + } + if d.HasChange("password") { + up.SetPassword(d.Get("password").(string)) + } + if d.HasChange("username") { + up.SetUsername(d.Get("username").(string)) + } + + _, err = cs.User.UpdateUser(up) + if err != nil { + return fmt.Errorf("Error updating user for Account %s: %s", d.Id(), err) + } + } + + log.Printf("[DEBUG] Account %s successfully updated", d.Id()) + return resourceCloudStackAccountRead(d, meta) +} + +// accountUser returns the user of the account whose username matches the given +// value, or nil when no user matches. CloudStack accounts can hold multiple +// users, so selecting by username keeps Read/Update operating on the user this +// resource manages instead of an arbitrary index. +func accountUser(account *cloudstack.Account, username string) *cloudstack.AccountUser { + if account == nil { + return nil + } + for i := range account.User { + if account.User[i].Username == username { + return &account.User[i] + } + } + return nil +} func resourceCloudStackAccountDelete(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index 197eaf4b..d82fd6e0 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -20,6 +20,7 @@ package cloudstack import ( + "fmt" "log" "github.com/apache/cloudstack-go/v2/cloudstack" @@ -44,6 +45,7 @@ func resourceCloudStackDiskOffering() *schema.Resource { "disk_size": { Type: schema.TypeInt, Required: true, + ForceNew: true, }, }, } @@ -72,8 +74,59 @@ func resourceCloudStackDiskOfferingCreate(d *schema.ResourceData, meta interface return resourceCloudStackDiskOfferingRead(d, meta) } -func resourceCloudStackDiskOfferingRead(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceCloudStackDiskOfferingRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Retrieving disk offering %s", d.Get("name").(string)) + + offering, count, err := cs.DiskOffering.GetDiskOfferingByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Disk offering %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving disk offering %s: %s", d.Id(), err) + } + + d.Set("name", offering.Name) + d.Set("display_text", offering.Displaytext) + d.Set("disk_size", offering.Disksize) + + return nil +} + +func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.DiskOffering.NewUpdateDiskOfferingParams(d.Id()) + + if d.HasChange("name") { + p.SetName(d.Get("name").(string)) + } + if d.HasChange("display_text") { + p.SetDisplaytext(d.Get("display_text").(string)) + } -func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface{}) error { return nil } + log.Printf("[DEBUG] Updating disk offering %s", d.Get("name").(string)) + _, err := cs.DiskOffering.UpdateDiskOffering(p) + if err != nil { + return fmt.Errorf("Error updating disk offering: %s", err) + } -func resourceCloudStackDiskOfferingDelete(d *schema.ResourceData, meta interface{}) error { return nil } + return resourceCloudStackDiskOfferingRead(d, meta) +} + +func resourceCloudStackDiskOfferingDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.DiskOffering.NewDeleteDiskOfferingParams(d.Id()) + + log.Printf("[DEBUG] Deleting disk offering %s", d.Get("name").(string)) + _, err := cs.DiskOffering.DeleteDiskOffering(p) + if err != nil { + return fmt.Errorf("Error deleting disk offering: %s", err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_domain.go b/cloudstack/resource_cloudstack_domain.go index 3e11b1a7..1c6b35a1 100644 --- a/cloudstack/resource_cloudstack_domain.go +++ b/cloudstack/resource_cloudstack_domain.go @@ -41,6 +41,8 @@ func resourceCloudStackDomain() *schema.Resource { "domain_id": { Type: schema.TypeString, Optional: true, + Computed: true, + ForceNew: true, }, "network_domain": { Type: schema.TypeString, @@ -49,6 +51,8 @@ func resourceCloudStackDomain() *schema.Resource { "parent_domain_id": { Type: schema.TypeString, Optional: true, + Computed: true, + ForceNew: true, }, }, } @@ -89,9 +93,62 @@ func resourceCloudStackDomainCreate(d *schema.ResourceData, meta interface{}) er return resourceCloudStackDomainRead(d, meta) } -func resourceCloudStackDomainRead(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceCloudStackDomainRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Reading Domain %s", d.Id()) + + p := cs.Domain.NewListDomainsParams() + p.SetId(d.Id()) + + domains, err := cs.Domain.ListDomains(p) + if err != nil { + return fmt.Errorf("Error reading Domain %s: %s", d.Id(), err) + } + + if domains.Count == 0 { + log.Printf("[DEBUG] Domain %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + domain := domains.Domains[0] + log.Printf("[DEBUG] Domain %s found: %s", d.Id(), domain.Name) + + d.Set("name", domain.Name) + d.Set("domain_id", domain.Id) + d.Set("network_domain", domain.Networkdomain) + d.Set("parent_domain_id", domain.Parentdomainid) + + return nil +} + +func resourceCloudStackDomainUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + + if d.HasChange("name") || d.HasChange("network_domain") { + p := cs.Domain.NewUpdateDomainParams(d.Id()) -func resourceCloudStackDomainUpdate(d *schema.ResourceData, meta interface{}) error { return nil } + if d.HasChange("name") { + p.SetName(name) + } + + if d.HasChange("network_domain") { + p.SetNetworkdomain(d.Get("network_domain").(string)) + } + + log.Printf("[DEBUG] Updating Domain %s", name) + _, err := cs.Domain.UpdateDomain(p) + if err != nil { + return fmt.Errorf("Error updating Domain %s: %s", name, err) + } + log.Printf("[DEBUG] Domain %s successfully updated", name) + } + + return resourceCloudStackDomainRead(d, meta) +} func resourceCloudStackDomainDelete(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) diff --git a/cloudstack/resource_cloudstack_user.go b/cloudstack/resource_cloudstack_user.go index 74fb84a9..a01a20a2 100644 --- a/cloudstack/resource_cloudstack_user.go +++ b/cloudstack/resource_cloudstack_user.go @@ -37,6 +37,7 @@ func resourceCloudStackUser() *schema.Resource { "account": { Type: schema.TypeString, Optional: true, + ForceNew: true, }, "email": { Type: schema.TypeString, @@ -51,8 +52,9 @@ func resourceCloudStackUser() *schema.Resource { Required: true, }, "password": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + Sensitive: true, }, "username": { Type: schema.TypeString, @@ -88,6 +90,31 @@ func resourceCloudStackUserCreate(d *schema.ResourceData, meta interface{}) erro } func resourceCloudStackUserUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.User.NewUpdateUserParams(d.Id()) + + if d.HasChange("email") { + p.SetEmail(d.Get("email").(string)) + } + if d.HasChange("first_name") { + p.SetFirstname(d.Get("first_name").(string)) + } + if d.HasChange("last_name") { + p.SetLastname(d.Get("last_name").(string)) + } + if d.HasChange("password") { + p.SetPassword(d.Get("password").(string)) + } + if d.HasChange("username") { + p.SetUsername(d.Get("username").(string)) + } + + _, err := cs.User.UpdateUser(p) + if err != nil { + return fmt.Errorf("Error updating User %s: %s", d.Id(), err) + } + return resourceCloudStackUserRead(d, meta) } @@ -106,5 +133,22 @@ func resourceCloudStackUserDelete(d *schema.ResourceData, meta interface{}) erro } func resourceCloudStackUserRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + user, count, err := cs.User.GetUserByID(d.Id()) + if err != nil { + if count == 0 { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading User %s: %s", d.Id(), err) + } + + d.Set("account", user.Account) + d.Set("email", user.Email) + d.Set("first_name", user.Firstname) + d.Set("last_name", user.Lastname) + d.Set("username", user.Username) + return nil }