diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8906805..f57d63e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: (echo; echo "Unexpected difference in directories after code generation. Run 'make generate' command and commit."; exit 1) # Run acceptance tests in a matrix with Terraform CLI versions - test: + acceptance-test: name: Terraform Provider Acceptance Tests needs: build runs-on: self-hosted @@ -82,7 +82,7 @@ jobs: # - '1.1.*' # - '1.2.*' # - '1.3.*' - - '1.4.*' + - '1.11.*' steps: - name: Debug info run: | @@ -107,11 +107,40 @@ jobs: terraform_version: ${{ matrix.terraform }} terraform_wrapper: false - run: go mod download + - name: Check Acceptance Test Environment + run: | + set -o allexport; eval "$(echo "$CI_CONFIG_HC_IP205_dos" | tr -s '\r\n' '\n')"; set +o allexport; + set -a + . ./internal/provider/tests/acceptance/setup/env.txt + go run ./internal/provider/tests/acceptance/setup/acceptance_test_env_prepare.go - env: TF_ACC: "1" run: | set -o allexport; eval "$(echo "$CI_CONFIG_HC_IP205_dos" | tr -s '\r\n' '\n')"; set +o allexport; echo cur-shell HC_HOST=$HC_HOST sh -c 'echo sub-shell HC_HOST=$HC_HOST' - go test -v -cover ./internal/provider/ + set -a + . ./internal/provider/tests/acceptance/setup/env.txt + go test -v -cover -coverpkg=github.com/hashicorp/terraform-provider-hypercore/internal/provider ./internal/provider/tests/acceptance/ timeout-minutes: 10 + - name: Cleanup Acceptance Test Environment + if: always() + run: | + set -o allexport; eval "$(echo "$CI_CONFIG_HC_IP205_dos" | tr -s '\r\n' '\n')"; set +o allexport; + set -a + . ./internal/provider/tests/acceptance/setup/env.txt + go run ./internal/provider/tests/acceptance/setup/acceptance_test_env_prepare.go "cleanup" + + unit-test: + name: Go Unit Tests + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed + with: + go-version-file: 'go.mod' + cache: true + - name: Run Unit Tests + run: go test -v -cover -coverpkg=github.com/hashicorp/terraform-provider-hypercore/internal/provider ./internal/provider/tests/unit/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 00bff46..85ee62c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -23,3 +23,12 @@ Add needed variables/secrets to github project: ``` TEMP: create VM named `testtf-src`. + +Prior to running acceptance tests we need to setup: + 1. Virtual machine + a. has one disk + b. has one nic + c. boot order is configured as [disk, nic] + 2. Virtual disk (as standalone not attached to the testing VM) + 3. Add names and UUIDs to the env.txt file in /tests/acceptance/setup directory + 4. Virtual machine needs to be powered off diff --git a/go.mod b/go.mod index 8d6a989..dd2ba0e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/hashicorp/terraform-plugin-go v0.25.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.11.0 + github.com/stretchr/testify v1.9.0 ) require ( @@ -67,4 +68,5 @@ require ( google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/provider/hypercore_vm_resource_test.go b/internal/provider/tests/acceptance/hypercore_vm_resource_acc_test.go similarity index 99% rename from internal/provider/hypercore_vm_resource_test.go rename to internal/provider/tests/acceptance/hypercore_vm_resource_acc_test.go index b365242..b776ae9 100644 --- a/internal/provider/hypercore_vm_resource_test.go +++ b/internal/provider/tests/acceptance/hypercore_vm_resource_acc_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package provider +package acceptance import ( "fmt" diff --git a/internal/provider/provider_test.go b/internal/provider/tests/acceptance/provider_acc_test.go similarity index 54% rename from internal/provider/provider_test.go rename to internal/provider/tests/acceptance/provider_acc_test.go index c204235..71d2b36 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/tests/acceptance/provider_acc_test.go @@ -1,25 +1,33 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package provider +package acceptance import ( "testing" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-provider-hypercore/internal/provider" ) +/* +var source_vm_name = os.Getenv("SOURCE_VM_NAME") +var existing_vdisk_uuid = os.Getenv("EXISTING_VDISK_UUID") +var source_nic_uuid = os.Getenv("SOURCE_NIC_UUID") +var source_disk_uuid = os.Getenv("SOURCE_DISK_UUID") +*/ + // testAccProtoV6ProviderFactories are used to instantiate a provider during // acceptance testing. The factory function will be invoked for every Terraform // CLI command executed to create a provider server to which the CLI can // reattach. var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ - "hypercore": providerserver.NewProtocol6WithError(New("test")()), + "hypercore": providerserver.NewProtocol6WithError(provider.New("test")()), } func testAccPreCheck(t *testing.T) { - // You can add code here to run prior to any test case execution, for example assertions - // about the appropriate environment variables being set are common to see in a pre-check - // function. + // Prechecks + // Don't use terraform CRUD operations here, this is ran prior to the test and will not cleanup + } diff --git a/internal/provider/tests/acceptance/setup/acceptance_test_env_prepare.go b/internal/provider/tests/acceptance/setup/acceptance_test_env_prepare.go new file mode 100644 index 0000000..9d4894d --- /dev/null +++ b/internal/provider/tests/acceptance/setup/acceptance_test_env_prepare.go @@ -0,0 +1,228 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "bytes" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "reflect" + "time" +) + +type EnvConfig struct { + SourceVmUUID string + ExistingVdiskUUID string + SourceVmName string + SourceDiskUUID string + SourceNicUUID string +} + +const ( + VirDomainEndpoint = "/rest/v1/VirDomain/" + VirtualDiskEndpoint = "/rest/v1/VirtualDisk/" + VirDomainActionEndpoint = "/rest/v1/VirDomain/action" +) + +func LoadEnv() EnvConfig { + return EnvConfig{ + SourceVmUUID: os.Getenv("SOURCE_VM_UUID"), + ExistingVdiskUUID: os.Getenv("EXISTING_VDISK_UUID"), + SourceVmName: os.Getenv("SOURCE_VM_NAME"), + SourceDiskUUID: os.Getenv("SOURCE_DISK_UUID"), + SourceNicUUID: os.Getenv("SOURCE_NIC_UUID"), + } +} + +func SetHTTPHeader(req *http.Request) *http.Request { + user := os.Getenv("HC_USERNAME") + pass := os.Getenv("HC_PASSWORD") + + // Create the Basic Authentication string + auth := user + ":" + pass + authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + + // Set the Content-Type header + req.Header.Set("Content-Type", "application/json") + + // Set the Content-Length header (not required, it's usually set automatically) + // req.Header.Set("Content-Length", fmt.Sprintf("%d", len(data))) + + // Set Basic Authentication header + req.Header.Set("Authorization", authHeader) + return req +} +func SetHTTPClient() *http.Client { + // Create a custom HTTP client with insecure transport + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, // Disable certificate verification + }, + } + client := &http.Client{Transport: tr} + + return client +} +func SendHTTPRequest(client *http.Client, method string, url string, data []byte) (*http.Response, []byte) { + req := SetHTTPMethod(method, url, data) + req = SetHTTPHeader(req) + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Sending request failed with %v", err) + } + defer resp.Body.Close() + + // Read and print the response + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Reading request response body failed with %v", err) + } + + fmt.Println("Response Status:", resp.Status) + fmt.Println("Response Body:", string(body)) + + return resp, body +} +func SetHTTPMethod(method string, url string, data []byte) *http.Request { + var req *http.Request + var err error + + // Set request method and body + if method == "GET" { + req, err = http.NewRequest("GET", url, bytes.NewBuffer(nil)) + } else { + req, err = http.NewRequest("POST", url, bytes.NewBuffer(data)) + } + + // Handle any errors that occur + if err != nil { + log.Fatalf("%s request to url: %s failed with error: %v", method, url, err) + } + + return req +} + +func AreEnvVariablesLoaded(env EnvConfig) bool { + if env.SourceVmUUID == "" || env.ExistingVdiskUUID == "" || env.SourceVmName == "" || env.SourceDiskUUID == "" || env.SourceNicUUID == "" { + return false + } + return true +} +func DoesTestVMExist(host string, client *http.Client, env EnvConfig) bool { + url := fmt.Sprintf("%s%s%s", host, VirDomainEndpoint, env.SourceVmUUID) + + resp, _ := SendHTTPRequest(client, "GET", url, nil) + + return resp.StatusCode == http.StatusOK +} +func IsTestVMRunning(host string, client *http.Client, env EnvConfig) bool { + url := fmt.Sprintf("%s%s%s", host, VirDomainEndpoint, env.SourceVmUUID) + + _, body := SendHTTPRequest(client, "GET", url, nil) + + var result []map[string]interface{} + err := json.Unmarshal(body, &result) + if err != nil { + log.Fatal(err) + } + return result[0]["state"] != "SHUTOFF" +} +func DoesVirtualDiskExist(host string, client *http.Client, env EnvConfig) bool { + url := fmt.Sprintf("%s%s%s", host, VirtualDiskEndpoint, env.ExistingVdiskUUID) + + resp, _ := SendHTTPRequest(client, "GET", url, nil) + + return resp.StatusCode == http.StatusOK +} +func IsBootOrderCorrect(host string, client *http.Client, env EnvConfig) bool { + expectedBootOrder := []string{env.SourceDiskUUID, env.SourceNicUUID} + url := fmt.Sprintf("%s%s%s", host, VirDomainEndpoint, env.SourceVmUUID) + + _, body := SendHTTPRequest(client, "GET", url, nil) + + var result []map[string]interface{} + err := json.Unmarshal(body, &result) + if err != nil { + log.Fatal(err) + } + return reflect.DeepEqual(result[0]["bootDevices"], expectedBootOrder) +} +func PrepareEnv(host string, client *http.Client, env EnvConfig) { + // We are doing env prepare here, make sure all the necessary entities are setup and present + if !AreEnvVariablesLoaded(env) { + log.Fatal("Environment variables aren't loaded, check env file in /acceptance/setup directory") + } else { + fmt.Println("Environment variables are loaded correctly") + } + if !DoesTestVMExist(host, client, env) { + log.Fatal("Acceptance test VM is missing in your testing environment") + } else { + fmt.Println("Acceptance test VM is present in the testing environment") + } + if IsTestVMRunning(host, client, env) { + log.Fatal("Acceptance test VM is RUNNING and should be turned off before the testing begins") + } else { + fmt.Println("Acceptance test VM is in the correct SHUTOFF state") + } + if !DoesVirtualDiskExist(host, client, env) { + log.Fatal("Acceptance test Virtual disk is missing in your testing environment") + } else { + fmt.Println("Acceptance test Virtual disk is present in your testing environment") + } + if IsBootOrderCorrect(host, client, env) { + log.Fatal("Acceptance test Boot order is incorrect on the test VM, should be disk followed by network interface") + } else { + fmt.Println("Acceptance test Boot order is in correct order") + } +} + +func CleanUpPowerState(host string, client *http.Client, env EnvConfig) { + data := []byte(fmt.Sprintf(`[{"virDomainUUID": "%s", "actionType": "STOP", "cause": "INTERNAL"}]`, env.SourceVmUUID)) + url := fmt.Sprintf("%s%s", host, VirDomainActionEndpoint) + SendHTTPRequest(client, "POST", url, data) + // wait 30 seconds for VM to shutdown and then proceed with other cleanup tasks + time.Sleep(30 * time.Second) +} +func CleanUpBootOrder(host string, client *http.Client, env EnvConfig) { + bootOrder := []string{env.SourceDiskUUID, env.SourceNicUUID} + payload := map[string]interface{}{ + "bootDevices": bootOrder, + } + data, err := json.Marshal(payload) + if err != nil { + log.Fatalf("Failed to marshal JSON: %v", err) + } + url := fmt.Sprintf("%s%s%s", host, VirDomainEndpoint, env.SourceVmUUID) + SendHTTPRequest(client, "POST", url, data) +} +func CleanupEnv(host string, client *http.Client, env EnvConfig) { + CleanUpPowerState(host, client, env) + CleanUpBootOrder(host, client, env) +} + +func main() { + /* + We are running env setup here based on the arguments passed into GO program it's either going to: + 1. Prepare environment + 2. Cleanup environment + Argument we are looking to pass is "cleanup" see test.yml workflow file for more information + */ + env := LoadEnv() + host := os.Getenv("HC_HOST") + client := SetHTTPClient() + isCleanup := len(os.Args) > 1 && os.Args[1] == "cleanup" + fmt.Println("Are we doing Cleanup:", isCleanup) + + if isCleanup { + CleanupEnv(host, client, env) + } else { + PrepareEnv(host, client, env) + } +} diff --git a/internal/provider/tests/acceptance/setup/env.txt b/internal/provider/tests/acceptance/setup/env.txt new file mode 100644 index 0000000..9f645d5 --- /dev/null +++ b/internal/provider/tests/acceptance/setup/env.txt @@ -0,0 +1,5 @@ +SOURCE_VM_UUID="97904009-1878-4881-b6df-83c85ab7dc1a" +EXISTING_VDISK_UUID="33c78baf-c3c6-4600-8432-9c7a2a3008ab" +SOURCE_VM_NAME="integration-test-vm" +SOURCE_NIC_UUID="7a79893f-129f-4d35-8800-35a947ff4a51" +SOURCE_DISK_UUID="46dac49c-9726-4e4f-950f-f2b06fcf2e24" diff --git a/internal/provider/tests/unit/hypercore_iso_resource_test.go b/internal/provider/tests/unit/hypercore_iso_resource_test.go new file mode 100644 index 0000000..3b53fe0 --- /dev/null +++ b/internal/provider/tests/unit/hypercore_iso_resource_test.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package unit + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-provider-hypercore/internal/provider" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/stretchr/testify/assert" +) + +func TestHypercoreISOResource_Schema(t *testing.T) { + // Create an instance of the resource + r := &provider.HypercoreISOResource{} + + // Prepare request and response objects + req := resource.SchemaRequest{} + resp := &resource.SchemaResponse{} + + // Call the Schema function + r.Schema(context.Background(), req, resp) + + // Validate the schema is set + assert.NotNil(t, resp.Schema) + assert.NotNil(t, resp.Schema.Attributes) + + // Check the description + assert.Contains(t, resp.Schema.MarkdownDescription, "Hypercore ISO resource to manage ISO images") + + // Check individual attributes + attributes := resp.Schema.Attributes + + // Check ID attribute + idAttr, ok := attributes["id"].(schema.StringAttribute) + assert.True(t, ok) + assert.True(t, idAttr.Computed) + assert.Contains(t, idAttr.MarkdownDescription, "ISO identifier") + + // Check Name attribute + nameAttr, ok := attributes["name"].(schema.StringAttribute) + assert.True(t, ok) + assert.True(t, nameAttr.Required) + assert.Contains(t, nameAttr.MarkdownDescription, "Desired name of the ISO to upload") + + // Check Source URL attribute + sourceURLAttr, ok := attributes["source_url"].(schema.StringAttribute) + assert.True(t, ok) + assert.True(t, sourceURLAttr.Optional) + assert.Contains(t, sourceURLAttr.MarkdownDescription, "Source URL from where to fetch") +}