diff --git a/.github/workflows/go-test-multiplatform.yml b/.github/workflows/go-test-multiplatform.yml index be77dce5..c22bb5a9 100644 --- a/.github/workflows/go-test-multiplatform.yml +++ b/.github/workflows/go-test-multiplatform.yml @@ -43,7 +43,7 @@ jobs: go-version: ${{ needs.get-go-version.outputs.go-version }} - run: | echo "Testing with Go ${{ needs.get-go-version.outputs.go-version }}" - go test -race -count 1 ./... -timeout=3m + go test -race -count 1 ./builder/linode/... -timeout=3m windows-go-tests: needs: @@ -57,7 +57,7 @@ jobs: go-version: ${{ needs.get-go-version.outputs.go-version }} - run: | echo "Testing with Go ${{ needs.get-go-version.outputs.go-version }}" - go test -race -count 1 ./... -timeout=3m + go test -race -count 1 ./builder/linode/... -timeout=3m linux-go-tests: needs: @@ -81,15 +81,17 @@ jobs: report_filename=$(date +'%Y%m%d%H%M')_packer_test_report.xml echo "REPORT_FILENAME=$report_filename" >> $GITHUB_ENV - - name: Run integration tests + - name: Run integration and unit tests run: | echo "Testing with Go ${{ needs.get-go-version.outputs.go-version }}" - if ! go test -race -count 1 ./... -timeout=3m -v | go-junit-report -set-exit-code > "$REPORT_FILENAME"; then + if ! make test | go-junit-report -set-exit-code > "$REPORT_FILENAME"; then echo "EXIT_STATUS=1" >> $GITHUB_ENV fi cat "$REPORT_FILENAME" + env: + LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} - - name: Upload test report + - name: Upload test report as artifact uses: actions/upload-artifact@v4 with: name: test-report-file @@ -111,6 +113,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' # Download the artifact generated by the 'linux-go-tests' job - name: Download test report @@ -133,7 +138,7 @@ jobs: - name: Add additional information to XML report run: | filename=$(ls | grep -E '^[0-9]{12}_packer_test_report\.xml$') - python scripts/add_to_xml_test_report.py \ + python tod_scripts/add_to_xml_test_report.py \ --branch_name "${{ env.RELEASE_VERSION }}" \ --gha_run_id "$GITHUB_RUN_ID" \ --gha_run_number "$GITHUB_RUN_NUMBER" \ @@ -143,7 +148,7 @@ jobs: - name: Upload test results to bucket run: | filename=$(ls | grep -E '^[0-9]{12}_packer_test_report\.xml$') - python3 scripts/test_report_upload_script.py "${filename}" + python3 tod_scripts/test_report_upload_script.py "${filename}" env: LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }} LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..df7dc11d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tod_scripts"] + path = tod_scripts + url = https://github.com/linode/TOD-test-report-uploader.git diff --git a/GNUmakefile b/Makefile similarity index 79% rename from GNUmakefile rename to Makefile index 1b081f5e..c31e2fce 100644 --- a/GNUmakefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME=linode BINARY=packer-plugin-${NAME} GOFMT_FILES?=$$(find . -name '*.go') COUNT?=1 -TEST?=$(shell go list ./...) +TEST?=$(shell go list ./builder/...) HASHICORP_PACKER_PLUGIN_SDK_VERSION?=$(shell go list -m github.com/hashicorp/packer-plugin-sdk | cut -d " " -f2) .PHONY: dev @@ -14,8 +14,8 @@ dev: build @mkdir -p ~/.packer.d/plugins/ @mv ${BINARY} ~/.packer.d/plugins/${BINARY} -test: fmtcheck - @go test -race -count $(COUNT) $(TEST) -timeout=3m +test: dev fmtcheck + @PACKER_ACC=1 go test -count $(COUNT) ./... -v -timeout=100m install-packer-sdc: ## Install packer sofware development command @go install github.com/hashicorp/packer-plugin-sdk/cmd/packer-sdc@${HASHICORP_PACKER_PLUGIN_SDK_VERSION} @@ -23,8 +23,11 @@ install-packer-sdc: ## Install packer sofware development command plugin-check: install-packer-sdc build @packer-sdc plugin-check ${BINARY} -testacc: dev - @PACKER_ACC=1 go test -count $(COUNT) -v $(TEST) -timeout=120m +unit-test: dev + @PACKER_ACC=1 go test -count $(COUNT) -v $(TEST) -timeout=10m + +int-test: dev + @go test -v test/integration/e2e_test.go generate: install-packer-sdc @go generate ./... diff --git a/go.mod b/go.mod index 8ee69dd6..6615aa1f 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,10 @@ require ( golang.org/x/oauth2 v0.16.0 ) -require github.com/mitchellh/mapstructure v1.5.0 +require ( + github.com/mitchellh/mapstructure v1.5.0 + github.com/stretchr/testify v1.8.4 +) require ( cloud.google.com/go v0.110.2 // indirect @@ -28,6 +31,7 @@ require ( github.com/aws/aws-sdk-go v1.44.114 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dylanmei/iso8601 v0.1.0 // indirect github.com/fatih/color v1.14.1 // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect @@ -79,6 +83,7 @@ require ( github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db // indirect github.com/pkg/sftp v1.13.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/ugorji/go/codec v1.2.6 // indirect github.com/ulikunitz/xz v0.5.10 // indirect @@ -98,6 +103,7 @@ require ( google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/zclconf/go-cty => github.com/nywilken/go-cty v1.13.3 // added by packer-sdc fix as noted in github.com/hashicorp/packer-plugin-sdk/issues/187 diff --git a/go.sum b/go.sum index 65c012c0..60caa550 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,7 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dylanmei/iso8601 v0.1.0 h1:812NGQDBcqquTfH5Yeo7lwR0nzx/cKdsmf3qMjPURUI= github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ= github.com/dylanmei/winrmtest v0.0.0-20210303004826-fbc9ae56efb6 h1:zWydSUQBJApHwpQ4guHi+mGyQN/8yN6xbKWdDtL3ZNM= @@ -292,6 +293,7 @@ github.com/pkg/sftp v1.13.2 h1:taJnKntsWgU+qae21Rx52lIwndAdKrj0mfUNQsz1z4Q= github.com/pkg/sftp v1.13.2/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -331,6 +333,7 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= diff --git a/scripts/add_to_xml_test_report.py b/scripts/add_to_xml_test_report.py deleted file mode 100644 index 41b4cef1..00000000 --- a/scripts/add_to_xml_test_report.py +++ /dev/null @@ -1,37 +0,0 @@ -import argparse -import xml.etree.ElementTree as ET - -# Parse command-line arguments -parser = argparse.ArgumentParser(description='Modify XML with workflow information') -parser.add_argument('--branch_name', required=True) -parser.add_argument('--gha_run_id', required=True) -parser.add_argument('--gha_run_number', required=True) -parser.add_argument('--xmlfile', required=True) # Added argument for XML file path - -args = parser.parse_args() - -# Open and parse the XML file -xml_file_path = args.xmlfile -tree = ET.parse(xml_file_path) -root = tree.getroot() - -# Create new elements for the information -branch_name_element = ET.Element('branch_name') -branch_name_element.text = args.branch_name - -gha_run_id_element = ET.Element('gha_run_id') -gha_run_id_element.text = args.gha_run_id - -gha_run_number_element = ET.Element('gha_run_number') -gha_run_number_element.text = args.gha_run_number - -# Add the new elements to the root of the XML -root.append(branch_name_element) -root.append(gha_run_id_element) -root.append(gha_run_number_element) - -# Save the modified XML -modified_xml_file_path = xml_file_path # Overwrite it -tree.write(modified_xml_file_path) - -print(f'Modified XML saved to {modified_xml_file_path}') diff --git a/scripts/test_report_upload_script.py b/scripts/test_report_upload_script.py deleted file mode 100644 index a3025835..00000000 --- a/scripts/test_report_upload_script.py +++ /dev/null @@ -1,81 +0,0 @@ -import boto3 -import sys -import os -import xml.etree.ElementTree as ET -from botocore.exceptions import NoCredentialsError - -ACCESS_KEY = os.environ.get('LINODE_CLI_OBJ_ACCESS_KEY') -SECRET_KEY = os.environ.get('LINODE_CLI_OBJ_SECRET_KEY') -BUCKET_NAME = 'dx-test-results' - -linode_obj_config = { - "aws_access_key_id": ACCESS_KEY, - "aws_secret_access_key": SECRET_KEY, - "endpoint_url": "https://us-southeast-1.linodeobjects.com", - "region_name": "us-southeast-1", -} - -def change_xml_report_to_tod_acceptable_version(file_name): - # Load the original XML file - tree = ET.parse(file_name) - root = tree.getroot() - - testsuites_element = root - - # total - total_tests = int(testsuites_element.get('tests')) if testsuites_element.get('tests') is not None else 0 - total_failures = int(testsuites_element.get('failures')) if testsuites_element.get('failures') is not None else 0 - total_errors = int(testsuites_element.get('errors')) if testsuites_element.get('errors') is not None else 0 - total_skipped = int(testsuites_element.get('skipped')) if testsuites_element.get('skipped') is not None else 0 - - # Create a new element with aggregated values - new_testsuites = ET.Element("testsuites") - new_testsuites.set("tests", str(total_tests)) - new_testsuites.set("failures", str(total_failures)) - new_testsuites.set("errors", str(total_errors)) - new_testsuites.set("skipped", str(total_skipped)) - - # Create a new element under - new_testsuite = ET.SubElement(new_testsuites, "testsuite", attrib=testsuites_element.attrib) - - for testcase in root.findall('.//testcase'): - new_testcase = ET.SubElement(new_testsuite, "testcase", attrib=testcase.attrib) - for child in testcase: - new_testcase.append(child) - - # Save the new XML to a file - try: - new_tree = ET.ElementTree(new_testsuites) - new_tree.write(file_name, encoding="UTF-8", xml_declaration=True) - print("XML content successfully over-written to " + file_name) - - except Exception as e: - print("Error writing XML content:", str(e)) - - - -def upload_to_linode_object_storage(file_name): - try: - s3 = boto3.client('s3', **linode_obj_config) - - s3.upload_file(Filename=file_name, Bucket=BUCKET_NAME, Key=file_name) - - print(f'Successfully uploaded {file_name} to Linode Object Storage.') - - except NoCredentialsError: - print('Credentials not available. Ensure you have set your AWS credentials.') - - -if __name__ == '__main__': - if len(sys.argv) != 2: - print('Usage: python upload_to_linode.py ') - sys.exit(1) - - file_name = sys.argv[1] - - if not file_name: - print('Error: The provided file name is empty or invalid.') - sys.exit(1) - - change_xml_report_to_tod_acceptable_version(file_name) - upload_to_linode_object_storage(file_name) \ No newline at end of file diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go new file mode 100644 index 00000000..b9a5ded8 --- /dev/null +++ b/test/integration/e2e_test.go @@ -0,0 +1,135 @@ +package integration + +import ( + "context" + "fmt" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + "github.com/linode/linodego" + "github.com/stretchr/testify/assert" + "golang.org/x/oauth2" +) + +const ( + packerTemplate = "template/test_image_template.json" +) + +func TestBuildPackerImage(t *testing.T) { + linodeToken := os.Getenv("LINODE_TOKEN") + + if linodeToken == "" { + t.Fatal("Linode token is not set. Please set LINODE_TOKEN as environment variable.") + } + + linodeImageLabel := generateImageLabel() + err := os.Setenv("LINODE_IMAGE_LABEL", linodeImageLabel) + if err != nil { + fmt.Printf("Error setting LINODE_IMAGE_LABEL: %v\n", err) + return + } + + // Run the Packer build command from terminal + cmd := exec.Command("packer", "build", packerTemplate) + + output, err := cmd.CombinedOutput() + + defer func() { + if err := teardown(linodeImageLabel); err != nil { + fmt.Printf("Error during deleting image after test execution: %v\n", err) + } + }() + + // Check if the Packer build was successful + if err != nil { + t.Fatalf("Error building Packer image: %v\nOutput:\n%s", err, output) + } + + // Assert the output contains expected strings + expectedSubstring := "Builds finished. The artifacts of successful builds" + assert.True(t, strings.Contains(string(output), expectedSubstring), "Expected successful build output to contain: %s", expectedSubstring) + + // Assert other fields + err = assertLinodeImage(linodeImageLabel, t) + + if err != nil { + t.Fatalf("Error asserting Linode builder image: %v", err) + } +} + +func assertLinodeImage(imageLabel string, t *testing.T) error { + client := getLinodegoClient() + + images, err := client.ListImages(context.Background(), nil) + if err != nil { + return fmt.Errorf("error listing Linode images: %v", err) + } + + // Find the desired image by label prefix + var targetImage *linodego.Image + for _, image := range images { + if image.Label != "" && strings.HasPrefix(image.Label, imageLabel) { + targetImage = &image + break + } + } + + if targetImage == nil { + return fmt.Errorf("image with label '%s' not found", imageLabel) + } + + assert.Equal(t, "manual", targetImage.Type, "unexpected instance type") + expectedInstanceIDFormat := "private/" + assert.True(t, strings.HasPrefix(targetImage.ID, expectedInstanceIDFormat), "unexpected instance ID prefix") + expectedInstanceLabel := "test-image-" + assert.True(t, strings.HasPrefix(targetImage.Label, expectedInstanceLabel), "unexpected instance label prefix") + expectedImageDescription := "My Test Image Description" + assert.Equal(t, expectedImageDescription, targetImage.Description, "unexpected image description") + + return nil +} + +func teardown(imageLabel string) error { + client := getLinodegoClient() + images, err := client.ListImages(context.Background(), nil) + if err != nil { + return fmt.Errorf("error listing Linode images: %v", err) + } + + for _, image := range images { + if image.Label != "" && strings.HasPrefix(image.Label, imageLabel) { + err = client.DeleteImage(context.Background(), image.ID) + if err != nil { + return fmt.Errorf("error during Linode image deletion: %v", err) + } + } + } + + return nil +} + +func getLinodegoClient() linodego.Client { + linodeToken := os.Getenv("LINODE_TOKEN") + + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: linodeToken}) + oauth2Client := &http.Client{ + Transport: &oauth2.Transport{ + Source: tokenSource, + }, + } + linodeClient := linodego.NewClient(oauth2Client) + + return linodeClient +} + +func generateImageLabel() string { + timestamp := strconv.FormatInt(time.Now().UnixNano(), 10) // Shortened format without dashes + instanceLabel := fmt.Sprintf("test-image-%s", timestamp) + + return instanceLabel +} diff --git a/test/integration/template/test_image_template.json b/test/integration/template/test_image_template.json new file mode 100644 index 00000000..7808bf31 --- /dev/null +++ b/test/integration/template/test_image_template.json @@ -0,0 +1,19 @@ +{ + "variables": { + "linode_token": "{{env `LINODE_TOKEN`}}", + "instance_image_label": "{{env `LINODE_IMAGE_LABEL`}}" + }, + "builders": [{ + "type": "linode", + "linode_token": "{{ user `linode_token` }}", + "image": "linode/debian9", + "region": "us-east", + "instance_type": "g6-nanode-1", + "instance_label": "test-instance-{{timestamp}}", + + "image_label": "{{ user `instance_image_label` }}", + "image_description": "My Test Image Description", + + "ssh_username": "root" + }] +} \ No newline at end of file diff --git a/tod_scripts b/tod_scripts new file mode 160000 index 00000000..eec4b995 --- /dev/null +++ b/tod_scripts @@ -0,0 +1 @@ +Subproject commit eec4b99557cef6f40e8b5b7de00357dc49fb041c