diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce34a42..4f0dc3b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ FEATURES: * resources/opennebula_virtual_network: allow to modify the user owning the resource (#529) * resources/opennebula_virtual_machine: add nil checks before type casting (#530) * resources/opennebula_virtual_router_nic: add floating_only nic argument (#547) +* resources/opennebula_service: add service role scaling (#553) ENHANCEMENTS: diff --git a/opennebula/resource_opennebula_service.go b/opennebula/resource_opennebula_service.go index d49525e7..5401f962 100644 --- a/opennebula/resource_opennebula_service.go +++ b/opennebula/resource_opennebula_service.go @@ -2,12 +2,15 @@ package opennebula import ( "context" + "encoding/json" "fmt" "log" "strconv" "strings" "time" + ver "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -31,6 +34,7 @@ func resourceOpennebulaService() *schema.Resource { Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(defaultServiceTimeout), Delete: schema.DefaultTimeout(defaultServiceTimeout), + Update: schema.DefaultTimeout(defaultServiceTimeout), }, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -52,7 +56,7 @@ func resourceOpennebulaService() *schema.Resource { "extra_template": { Type: schema.TypeString, Optional: true, - ForceNew: true, + ForceNew: false, Description: "Extra template information in json format to be added to the service template during instantiate.", }, "permissions": { @@ -587,6 +591,81 @@ func resourceOpennebulaServiceUpdate(ctx context.Context, d *schema.ResourceData log.Printf("[INFO] Successfully updated owner for Service %s\n", service.Name) } + if d.HasChange("extra_template") { + extra_template := make(map[string]interface{}) + if v, ok := d.GetOk("extra_template"); ok { + if err := json.Unmarshal([]byte(v.(string)), &extra_template); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to parse extra template", + Detail: fmt.Sprintf("service (ID: %s): %s", d.Id(), err), + }) + return diags + } + } + + type roleDesc struct { + name string + oldCardinality int + newCardinality int + wantScale bool + } + + desc := []roleDesc{} + + for _, role := range service.Template.Body.Roles { + desc = append(desc, roleDesc{ + name: role.Name, + oldCardinality: role.Cardinality, + }) + } + + if roles, ok := extra_template["roles"]; ok { + for k := 0; k < len(desc) && k < len(roles.([]interface{})); k++ { + if v, ok := roles.([]interface{})[k].(map[string]interface{})["cardinality"]; ok { + desc[k].newCardinality = int(v.(float64)) + desc[k].wantScale = desc[k].newCardinality != desc[k].oldCardinality + } + } + } + + minVersion, _ := ver.NewVersion("6.8.0") + + for _, v := range desc { + if v.wantScale { + if config.OneVersion.LessThan(minVersion) { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Role scaling is unsupported for this environment", + Detail: fmt.Sprintf("service (ID: %s): %s", d.Id(), err), + }) + return diags + } + + if err := sc.Scale(v.name, v.newCardinality, false); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to scale role", + Detail: fmt.Sprintf("service (ID: %s): %s", d.Id(), err), + }) + return diags + } + + timeout := d.Timeout(schema.TimeoutUpdate) + if _, err := waitForServiceState(ctx, d, meta, "running", timeout); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to wait service to be in RUNNING state", + Detail: fmt.Sprintf("service (ID: %s): %s", d.Id(), err), + }) + return diags + } + } + } + + log.Printf("[INFO] Successfully scaled roles of Service %s\n", service.Name) + } + return resourceOpennebulaServiceRead(ctx, d, meta) } @@ -680,7 +759,8 @@ func waitForServiceState(ctx context.Context, d *schema.ResourceData, meta inter log.Printf("Waiting for Service (%s) to be in state %s", d.Id(), state) stateConf := &resource.StateChangeConf{ - Pending: []string{"anythingelse"}, Target: []string{state}, + Pending: []string{"anythingelse", "cooldown"}, + Target: []string{state}, Refresh: func() (interface{}, string, error) { log.Println("Refreshing Service state...") if d.Id() != "" { @@ -734,5 +814,4 @@ func waitForServiceState(ctx context.Context, d *schema.ResourceData, meta inter } return stateConf.WaitForStateContext(ctx) - } diff --git a/opennebula/resource_opennebula_service_scale_test.go b/opennebula/resource_opennebula_service_scale_test.go new file mode 100644 index 00000000..a4177340 --- /dev/null +++ b/opennebula/resource_opennebula_service_scale_test.go @@ -0,0 +1,148 @@ +package opennebula + +import ( + "context" + "testing" + + ver "github.com/hashicorp/go-version" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// NOTE: OneFlow role template merging is unavailable in OpenNebula releases prior to 6.8.0. +func preCheck(t *testing.T) { + testAccPreCheck(t) + + if err := testAccProvider.Configure(context.Background(), terraform.NewResourceConfigRaw(nil)); err != nil { + t.Fatal(err) + } + + config := testAccProvider.Meta().(*Configuration) + + minVersion, _ := ver.NewVersion("6.8.0") + + if config.OneVersion.LessThan(minVersion) { + t.Skip() + } +} + +func TestAccServiceScale(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { preCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccServiceScaleConfigBasic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_service.test", "name", "service-scale-test-tf"), + resource.TestCheckResourceAttr("opennebula_service.test", "roles.0.cardinality", "0"), + resource.TestCheckResourceAttr("opennebula_service.test", "roles.1.cardinality", "0"), + resource.TestCheckResourceAttrSet("opennebula_service.test", "state"), + resource.TestCheckResourceAttrSet("opennebula_service.test", "template_id"), + ), + }, + { + Config: testAccServiceScaleConfigUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_service.test", "name", "service-scale-test-tf"), + resource.TestCheckResourceAttr("opennebula_service.test", "roles.0.cardinality", "1"), + resource.TestCheckResourceAttr("opennebula_service.test", "roles.1.cardinality", "1"), + resource.TestCheckResourceAttrSet("opennebula_service.test", "state"), + resource.TestCheckResourceAttrSet("opennebula_service.test", "template_id"), + ), + }, + }, + }) +} + +var testAccServiceScaleVMTemplate = ` + +resource "opennebula_template" "test" { + name = "service-scale-test-tf" + + cpu = 1 + vcpu = 1 + memory = 64 + + graphics { + keymap = "en-us" + listen = "0.0.0.0" + type = "VNC" + } + + os { + arch = "x86_64" + boot = "" + } +} +` + +var testAccServiceScaleTemplate = ` + +resource "opennebula_service_template" "test" { + name = "service-scale-test-tf" + template = jsonencode({ + TEMPLATE = { + BODY = { + name = "service" + deployment = "straight" + roles = [ + { + name = "role0" + cooldown = 5 # seconds + vm_template = tonumber(opennebula_template.test.id) + }, + { + name = "role1" + parents = ["role0"] + cooldown = 5 # seconds + vm_template = tonumber(opennebula_template.test.id) + }, + ] + } + } + }) + lifecycle { + ignore_changes = all + } +} +` + +var testAccServiceScaleConfigBasic = testAccServiceScaleVMTemplate + testAccServiceScaleTemplate + ` + +resource "opennebula_service" "test" { + name = "service-scale-test-tf" + template_id = opennebula_service_template.test.id + extra_template = jsonencode({ + roles = [ + { cardinality = 0 }, + { cardinality = 0 }, + ] + }) + timeouts { + create = "2m" + delete = "2m" + update = "2m" + } +} +` + +var testAccServiceScaleConfigUpdate = testAccServiceScaleVMTemplate + testAccServiceScaleTemplate + ` + +resource "opennebula_service" "test" { + name = "service-scale-test-tf" + template_id = opennebula_service_template.test.id + extra_template = jsonencode({ + roles = [ + { cardinality = 1 }, + { cardinality = 1 }, + ] + }) + timeouts { + create = "2m" + delete = "2m" + update = "2m" + } +} +`