[compute] add hostname option to compute instance#1910
[compute] add hostname option to compute instance#1910hops wants to merge 1 commit intoterraform-provider-openstack:mainfrom
hostname option to compute instance#1910Conversation
|
@hops thanks for the PR. According to nova API it is possible to update the hostname. Can you implement the hostname update logic as well? https://docs.openstack.org/api-ref/compute/#update-server |
|
@kayrus unless I'm missing something that would require adding update support in gophercloud as the hostname is missing in the UpdateOpts type. I can add this to gophercloud and push it upstream unless you see another solution? |
|
@hops feel free to push this to gophercloud |
|
@kayrus I saw you already backported my change to the gophercloud v2 branch, thanks! Should I bump the gophercloud version in this pull request as a separate commit or in a separate pull request? |
|
@hops no worries, this will be addressed in #1899. we can implement the remaining updates later without needing a gophercloud update. my goal was to cover all the caveats at once, although that hadn't been done previously.
feel free to push it here, but please be prepared to wait longer for tests |
67f8891 to
ba6e856
Compare
kayrus
left a comment
There was a problem hiding this comment.
thanks for your effort. see my comments regarding the code.
|
in order to avoid long waiting, I'm gonna to update gophercloud in a separate PR. |
ba6e856 to
e44d3df
Compare
|
@kayrus I rebased my branch and changed your suggestions from the review. The update hostname acceptance test only checks if the instance ID hasn't changed as we're still missing Last but not least, thank you for your fast response time and sorry that I can't keep up with your pace. |
|
@hops please fix the golangci issues |
e44d3df to
5643982
Compare
|
@kayrus I've fixed the golangci issues and added the microversion handling for flavors in |
| Config: testAccComputeInstanceV2HostnameFqdnConfig(), | ||
| Check: resource.ComposeTestCheckFunc( | ||
| testAccCheckComputeV2InstanceExists(t.Context(), "openstack_compute_instance_v2.instance_1", &instance), | ||
| // resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", instance.Hostname), |
There was a problem hiding this comment.
please try this
| // resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", instance.Hostname), | |
| resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", &instance.Hostname), |
There was a problem hiding this comment.
That won't work as Hostname is already a pointer. https://github.com/gophercloud/gophercloud/blob/main/openstack/compute/v2/servers/results.go#L260
There was a problem hiding this comment.
the testAccCheckComputeV2InstanceExists doesn't use microversion fetching the server resource. you need to create a new testAccCheckComputeV2InstanceHostnameExists and set service client microversion to fetch the hostname.
There was a problem hiding this comment.
I've tested this as well by adding the computeV2InstanceCreateServerWithHostnameMicroversion here with the same result (nil pointer dereference). I've also added it as a test to testAccCheckComputeV2InstanceDestroy without luck.
Do you know if there's something else in the test suite that calls GET requests to the instance?
There was a problem hiding this comment.
@kayrus I've attached the Terraform debug and trace log from running the TestAccComputeV2Instance_hostnameFqdn acceptance test. I do see GET requests to the /compute/v2.1/servers/<UUID> with the header X-Openstack-Nova-Api-Version: 2.1 but I can't figure out who's sending these. I hope you have an idea or maybe a pointer on how to track down the caller of these requests. Also here's the terminal output of the test run.
$ make testacc TEST=./openstack TESTARGS="-run=TestAccComputeV2Instance_hostnameFqdn -count=1"
==> Checking that code complies with gofmt requirements...
TF_ACC=1 go test ./openstack -v -run=TestAccComputeV2Instance_hostnameFqdn -count=1 -timeout 120m
=== RUN TestAccComputeV2Instance_hostnameFqdn
--- FAIL: TestAccComputeV2Instance_hostnameFqdn (33.28s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x102e29f20]
goroutine 4 [running]:
testing.tRunner.func1.2({0x10344e520, 0x10419f4a0})
/opt/homebrew/Cellar/go/1.24.5/libexec/src/testing/testing.go:1734 +0x1ac
testing.tRunner.func1()
/opt/homebrew/Cellar/go/1.24.5/libexec/src/testing/testing.go:1737 +0x334
panic({0x10344e520?, 0x10419f4a0?})
/opt/homebrew/Cellar/go/1.24.5/libexec/src/runtime/panic.go:792 +0x124
github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack.TestAccComputeV2Instance_hostnameFqdn.TestCheckResourceAttrPtr.func4(0x140005db9d0)
/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing.go:1746 +0x30
github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack.TestAccComputeV2Instance_hostnameFqdn.ComposeTestCheckFunc.func5(0x140005db9d0)
/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing.go:1020 +0x64
github.com/hashicorp/terraform-plugin-testing/helper/resource.testStepNewConfig({_, _}, {_, _}, {0x0, 0x1400043b8a0, {0x0, 0x0, 0x0}, 0x140004277a0, ...}, ...)
/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing_new_config.go:219 +0xd80
github.com/hashicorp/terraform-plugin-testing/helper/resource.runNewTest({0x103649648, 0x140002a28d0}, {0x103652b20, 0x14000003a40}, {0x0, 0x1400043b8a0, {0x0, 0x0, 0x0}, 0x140004277a0, ...}, ...)
/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing_new.go:362 +0x1dcc
github.com/hashicorp/terraform-plugin-testing/helper/resource.Test({0x103652b20, 0x14000003a40}, {0x0, 0x1400043b8a0, {0x0, 0x0, 0x0}, 0x140004277a0, 0x0, 0x0, ...})
/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing.go:979 +0x510
github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack.TestAccComputeV2Instance_hostnameFqdn(0x14000003a40)
/Users/user/src/labservices/ls-terraform-openstack-provider/openstack/resource_openstack_compute_instance_v2_test.go:901 +0x3c8
testing.tRunner(0x14000003a40, 0x10362e148)
/opt/homebrew/Cellar/go/1.24.5/libexec/src/testing/testing.go:1792 +0xe4
created by testing.(*T).Run in goroutine 1
/opt/homebrew/Cellar/go/1.24.5/libexec/src/testing/testing.go:1851 +0x374
FAIL github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack 33.641s
FAIL
make: *** [testacc] Error 1
The microversion on the compute client in testAccCheckComputeV2InstanceExists and testAccCheckComputeV2InstanceDestroy are only temporary and I'll create proper functions once the culprit of the error has been identified.
There was a problem hiding this comment.
in resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", instance.Hostname), you're passing a nil pointer.
in testAccCheckComputeV2InstanceExists(t.Context(), "openstack_compute_instance_v2.instance_1", &instance), you're passing a pointer to a struct and then populate this struct with new values. the nil get overwritten with a new pointer, but old nil pointer was already passed to a TestCheckResourceAttrPtr func.
try this approach:
diff --git a/openstack/resource_openstack_compute_instance_v2_test.go b/openstack/resource_openstack_compute_instance_v2_test.go
index 7cc7fc87..06b0bced 100644
--- a/openstack/resource_openstack_compute_instance_v2_test.go
+++ b/openstack/resource_openstack_compute_instance_v2_test.go
@@ -910,13 +910,25 @@ func TestAccComputeV2Instance_hostnameFqdn(t *testing.T) {
Config: testAccComputeInstanceV2HostnameFqdnConfig(),
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2InstanceExists(t.Context(), "openstack_compute_instance_v2.instance_1", &instance),
- resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", instance.Hostname),
+ resource.TestCheckResourceAttrWith("openstack_compute_instance_v2.instance_1", "hostname", testAccCheckComputeV2InstanceHostname(&instance)),
),
},
},
})
}
+func testAccCheckComputeV2InstanceHostname(instance *servers.Server) resource.CheckResourceAttrWithFunc {
+ return func(v string) error {
+ if instance.Hostname == nil {
+ return fmt.Errorf("empty hostname")
+ }
+ if *instance.Hostname != v {
+ return fmt.Errorf("unexpected hostname: %s", *instance.Hostname)
+ }
+ return nil
+ }
+}
+
func testAccCheckComputeV2InstanceDestroy(ctx context.Context) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)There was a problem hiding this comment.
Well, that's embarrassing but totally makes sense. Thank you for your guidance!
I've fixed the acceptance tests with your suggestion and they passed on a DevStack 2025.1 environment. I also squashed my commits.
b4a5337 to
e3e2b22
Compare
kayrus
left a comment
There was a problem hiding this comment.
Thanks for your effort. See my PR comments.
| hostname = v.(string) | ||
| if isValidHostname(hostname) { | ||
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion | ||
| } else { | ||
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion | ||
| } |
There was a problem hiding this comment.
| hostname = v.(string) | |
| if isValidHostname(hostname) { | |
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion | |
| } else { | |
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion | |
| } | |
| hostname = v.(string) | |
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion | |
| if isValidHostname(hostname) { | |
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion | |
| } |
| server, err := servers.Get(ctx, computeClient, d.Id()).Extract() | ||
| if err != nil { | ||
| return diag.FromErr(CheckDeleted(d, err, "server")) | ||
| log.Printf("[DEBUG] Falling back to the default API call due to: %#v", err) |
There was a problem hiding this comment.
| log.Printf("[DEBUG] Falling back to the default API call due to: %#v", err) | |
| log.Printf("[DEBUG] Falling back to the default API microversion due to: %#v", err) |
| // Set the required microversion. | ||
| if isValidHostname(*updateOpts.Hostname) { | ||
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion | ||
| } else { | ||
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion | ||
| } |
There was a problem hiding this comment.
| // Set the required microversion. | |
| if isValidHostname(*updateOpts.Hostname) { | |
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion | |
| } else { | |
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion | |
| } | |
| // Set the required microversion. | |
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion | |
| if isValidHostname(*updateOpts.Hostname) { | |
| computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion | |
| } |
| var e gophercloud.ErrResourceNotFound | ||
| if errors.Is(err, &e) { |
There was a problem hiding this comment.
doesn't the if gophercloud.ResponseCodeIs(err, http.StatusNotFound) { work here?
and how about this? let's omit info about microversions here
flavorName := server.Flavor["original_name"].(string)
flavorID := server.Flavor["id"].(string)
if flavorName != "" {
// Starting with microversion 2.47 Nova returns the flavor name but not the flavor ID anymore
d.Set("flavor_name", flavorName)
v, err := flavorsutils.IDFromName(ctx, computeClient, flavorName)
if err != nil {
if !gophercloud.ResponseCodeIs(err, http.StatusNotFound){
return diag.FromErr(err)
}
// Original flavor was deleted, but it is possible that instance started
// with this flavor is still running
log.Printf("[DEBUG] Original flavor of instance with id %s could not be found", d.Id())
}
if v != "" {
d.Set("flavor_id", v)
}
} else if flavorID != "" {
// handle case, when microversion was not set
d.Set("flavor_id", flavorID)
v, err := flavors.Get(ctx, computeClient, flavorID).Extract()
if err != nil {
if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
return diag.FromErr(err)
}
// Original flavor was deleted, but it is possible that instance started
// with this flavor is still running
log.Printf("[DEBUG] Original flavor of instance with id %s could not be found", d.Id())
}
if v != nil && v.Name != "" {
d.Set("flavor_name", v.Name)
}
}
if flavorName == "" || flavorID == "" {
return diag.Errorf("Error setting OpenStack server's flavor: %v", server.Flavor)
}There was a problem hiding this comment.
No if gophercloud.ResponseCodeIs(err, http.StatusNotFound) doesn't work because IDFromName() doesn't return a ErrUnexpectedResponsecode like flavors.Get() does.
IDFromName() from gophercloud utils returns a ErrResourceNotFound{Name: name, ResourceType: "flavor"} with the name being the flavor's name.
Generally I like your approach more but have a couple of remarks.
flavorName := server.Flavor["original_name"].(string)
flavorID := server.Flavor["id"].(string)This will always cause a type assertion error because either we have the ID (microversion <= 2.46) or the name (microversion >= 2.47) but never both at the same time. I'd suggest to leave it as is (flavorID, ok := server.Flavor["id"].(string)) but I'm open to other suggestions.
if flavorName == "" || flavorID == "" {
return diag.Errorf("Error setting OpenStack server's flavor: %v", server.Flavor)
}Same issue as above. Did you mean to write if flavorName == "" && flavorID == ""?
Further I think we have to keep the reset of flavor_id and flavor_name in case the flavor got deleted but the instance is still running. Otherwise it wouldn't behave the same as before this change.
There was a problem hiding this comment.
I'd suggest to leave it as is (flavorID, ok := server.Flavor["id"].(string))
agree
Did you mean to write if flavorName == "" && flavorID == ""?
yes
Further I think we have to keep the reset of flavor_id and flavor_name in case the flavor got deleted but the instance is still running. Otherwise it wouldn't behave the same as before this change.
ok, I'll check this again in the next review cycle.
e3e2b22 to
856d514
Compare
This pull request adds the optional
hostnameoption to the compute_instance resource. I've marked this as a draft as I'd like to get your input on a couple of things.The
computeClientused inresourceComputeInstanceV2Readdoes not set any microversion and thus theOS-EXT-SRV-ATTR:hostnameis not returned by the OpenStack API. TheOS-EXT-SRV-ATTR:hostnameis available from microversion 2.3 but until version 2.90 it was only returned to administrator users. That shouldn't be an issue because until microversion 2.90 the hostname could not be specified anyway and was generated from the instance's name.I'm not sure what the best solution to this problem is. Maybe an additional parameter to pass the microversion which would mean we have to change the calls in
resourceComputeInstanceV2UpdateandresourceComputeInstanceV2ImportStateas well.Another important detail is that with microversion 2.47 the response of the flavor changed from
flavor.idandflavor.ilnksto an object with the individual fields of a flavor. However the flavor ID is not available anymore.I've tested these changes against a DevStack 2025.1 installation and against a real OpenStack environment running 2024.1. On DevStack I ran all acceptance tests beginning with
testAccCheckComputeV2Instancewithout errors. The tests on the OpenStack environment were done manually.TestCheckResourceAttr) as it isn't returned by the API.dataSourceComputeInstanceV2Readfunction also not setting any microversion.