diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 2260b408b..e798cca5f 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -42,11 +42,11 @@ jobs: run: | case "${{ matrix.user }}" in "USER_1") - echo "TEST_TAGS=acceptance,backup,domain,domainrecord,domains,domainzonefile,helper,instance" >> $GITHUB_ENV + echo "TEST_TAGS=acceptance,backup,domain,domainrecord,domains,domainzonefile,helper,instance,provider" >> $GITHUB_ENV echo "LINODE_TOKEN=${{ secrets.LINODE_TOKEN_USER_1 }}" >> $GITHUB_ENV ;; "USER_2") - echo "TEST_TAGS=firewall,firewalldevice,firewalls,image,images,instancenetworking,instancesharedips,instancetype,instancetypes,ipv6range,ipv6ranges,kernel,kernels,nb,nbconfig,nbconfigs,nbnode,nbs,sshkey,sshkeys,vlan,volume,volumes,vpc,vpcs" >> $GITHUB_ENV + echo "TEST_TAGS=firewall,firewalldevice,firewalls,image,images,instancenetworking,instancesharedips,instancetype,instancetypes,ipv6range,ipv6ranges,kernel,kernels,nb,nbconfig,nbconfigs,nbnode,nbs,sshkey,sshkeys,vlan,volume,volumes,vpc,vpcs,vpcsubnets" >> $GITHUB_ENV echo "LINODE_TOKEN=${{ secrets.LINODE_TOKEN_USER_2 }}" >> $GITHUB_ENV ;; "USER_3") @@ -81,7 +81,7 @@ jobs: process-upload-report: runs-on: ubuntu-latest needs: [integration_tests] - if: always() # Run even if previous job fails + if: always() && github.repository == 'linode/terraform-provider-linode' # Run even if integration tests fail and only on main repository steps: - name: Checkout code @@ -145,4 +145,70 @@ jobs: run: | cd e2e_scripts/cloud_security_scripts/lke_calico_rules/ && ./lke_calico_rules_e2e.sh env: - LINODE_TOKEN: ${{ secrets.LINODE_TOKEN_USER_4 }} \ No newline at end of file + LINODE_TOKEN: ${{ secrets.LINODE_TOKEN_USER_4 }} + + notify-slack: + runs-on: ubuntu-latest + needs: [integration_tests] + if: always() && github.repository == 'linode/terraform-provider-linode' # Run even if integration tests fail and only on main repository + + steps: + - name: Notify Slack + uses: slackapi/slack-github-action@v1.27.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":rocket: *${{ github.workflow }} Completed in: ${{ github.repository }}* :white_check_mark:" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Build Result:*\n${{ needs.integration_tests.result == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}" + }, + { + "type": "mrkdwn", + "text": "*Branch:*\n`${{ github.ref_name }}`" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Commit Hash:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>" + }, + { + "type": "mrkdwn", + "text": "*Run URL:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Triggered by: :bust_in_silhouette: `${{ github.actor }}`" + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/nightly_smoke_tests.yml b/.github/workflows/nightly_smoke_tests.yml index 076f3e2d0..21f12d1cc 100644 --- a/.github/workflows/nightly_smoke_tests.yml +++ b/.github/workflows/nightly_smoke_tests.yml @@ -4,25 +4,99 @@ on: schedule: - cron: "0 0 * * *" workflow_dispatch: + inputs: + sha: + description: 'Commit SHA to test' + required: false + default: '' + type: string jobs: smoke_tests: + if: github.repository == 'linode/terraform-provider-linode' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: - ref: dev + fetch-depth: 0 + submodules: 'recursive' + ref: ${{ github.event.inputs.sha || github.ref }} - - name: Set up go + - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 'stable' + go-version: '1.x' - - run: go version + - name: Install Dependencies + run: | + make deps - name: Run smoke tests - run: make smoke-test + id: smoke_tests + run: | + make smoke-test env: LINODE_TOKEN: ${{ secrets.DX_LINODE_TOKEN }} + + - name: Notify Slack + if: always() + uses: slackapi/slack-github-action@v1.27.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":rocket: *${{ github.workflow }} Completed in: ${{ github.repository }}* :white_check_mark:" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Build Result:*\n${{ steps.smoke_tests.outcome == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}" + }, + { + "type": "mrkdwn", + "text": "*Branch:*\n`${{ github.ref_name }}`" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Commit Hash:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>" + }, + { + "type": "mrkdwn", + "text": "*Run URL:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Triggered by: :bust_in_silhouette: `${{ github.actor }}`" + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/Makefile b/Makefile index 7e3c93403..9e325c27f 100644 --- a/Makefile +++ b/Makefile @@ -78,16 +78,34 @@ include-env: $(IP_ENV_FILE) generate-ip-env-fw-e2e: $(IP_ENV_FILE) +SUBMODULE_DIR := e2e_scripts + $(IP_ENV_FILE): # Generate env file for E2E cloud firewall - . ./e2e_scripts/cloud_security_scripts/cloud_e2e_firewall/terraform-provider-linode/generate_ip_env_fw_e2e.sh || touch $(IP_ENV_FILE) + @if [ ! -d $(SUBMODULE_DIR) ]; then \ + echo "Submodule directory $(SUBMODULE_DIR) does not exist. Updating submodules..."; \ + git submodule update --init --recursive; \ + else \ + echo "Submodule directory $(SUBMODULE_DIR) already exists. Skipping update."; \ + fi + . ./e2e_scripts/cloud_security_scripts/cloud_e2e_firewall/terraform-provider-linode/generate_ip_env_fw_e2e.sh .PHONY: smoke-test -smoke-test: fmt-check +smoke-test: fmt-check generate-ip-env-fw-e2e include-env TF_ACC=1 \ LINODE_API_VERSION="v4beta" \ RUN_LONG_TESTS=$(RUN_LONG_TESTS) \ - go test -v -run smoke ./linode/... -count $(COUNT) -timeout $(TIMEOUT) -parallel=$(PARALLEL) -ldflags="-X=github.com/linode/terraform-provider-linode/v2/version.ProviderVersion=acc" + TF_VAR_ipv4_addr=${PUBLIC_IPV4} \ + TF_VAR_ipv6_addr=${PUBLIC_IPV6} \ + bash -c 'set -o pipefail && go test -v ./linode/... -run TestSmokeTests -tags=integration \ + -count $(COUNT) \ + -timeout $(TIMEOUT) \ + -parallel=$(PARALLEL) \ + -ldflags="-X=github.com/linode/terraform-provider-linode/v2/version.ProviderVersion=acc" \ + | sed -e "/testing: warning: no tests to run/,+1d" -e "/\[no test files\]/d" -e "/\[no tests to run\]/d"; \ + exit_status=$$?; \ + exit $$exit_status' + .PHONY: docs-check docs-check: diff --git a/docs/data-sources/instances.md b/docs/data-sources/instances.md index 57d796926..da947af8f 100644 --- a/docs/data-sources/instances.md +++ b/docs/data-sources/instances.md @@ -75,6 +75,8 @@ Each Linode instance will be stored in the `instances` attribute and will export * `tags` - A list of tags applied to this object. Tags are case-insensitive and are for organizational purposes only. +* `capabilities` - A list of capabilities of this Linode instance. + * `private_ip` - If true, the Linode has private networking enabled, allowing use of the 192.168.128.0/17 network within the Linode's region. * `alerts.0.cpu` - The percentage of CPU usage required to trigger an alert. If the average CPU usage over two hours exceeds this value, we'll send you an alert. If this is set to 0, the alert is disabled. diff --git a/docs/data-sources/volume.md b/docs/data-sources/volume.md index 7742cf7a7..308695a79 100644 --- a/docs/data-sources/volume.md +++ b/docs/data-sources/volume.md @@ -48,3 +48,5 @@ The Linode Volume resource exports the following attributes: - `linode_id` - If a Volume is attached to a specific Linode, the ID of that Linode will be displayed here. If the Volume is unattached, this value will be null. - `filesystem_path` - The full filesystem path for the Volume based on the Volume's label. Path is /dev/disk/by-id/scsi-0LinodeVolume + Volume label. + +- `encryption` - Whether Block Storage Disk Encryption is enabled or disabled on this Volume. Note: Block Storage Disk Encryption is not currently available to all users. diff --git a/docs/index.md b/docs/index.md index 6d1c4886a..1d94bc100 100644 --- a/docs/index.md +++ b/docs/index.md @@ -80,6 +80,10 @@ This section outlines commonly used provider configuration options. The Linode API version can also be specified using the `LINODE_API_VERSION` environment variable. +* `api_ca_path` (Optional) The path to a CA file to trust when making API requests. + + The Linode API CA file path can also be specified using the `LINODE_CA` environment variable. + * `obj_access_key` - (Optional) The access key to be used in [linode_object_storage_bucket](/docs/resources/object_storage_bucket.md) and [linode_object_storage_object](/docs/resources/object_storage_object.md). The Object Access Key can also be specified using the `LINODE_OBJ_ACCESS_KEY` shell environment variable. diff --git a/docs/resources/volume.md b/docs/resources/volume.md index 3dd2d3bcc..fe2f3a760 100644 --- a/docs/resources/volume.md +++ b/docs/resources/volume.md @@ -81,6 +81,8 @@ The following arguments are supported: * `tags` - (Optional) A list of tags applied to this object. Tags are case-insensitive and are for organizational purposes only. +* `encryption` - (Optional) Whether Block Storage Disk Encryption is enabled or disabled on this Volume. Note: Block Storage Disk Encryption is not currently available to all users. + ### Timeouts The `timeouts` block allows you to specify [timeouts](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts) for certain actions: diff --git a/go.mod b/go.mod index 1a858fc88..e4085c31d 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.22.0 toolchain go1.22.5 require ( - github.com/aws/aws-sdk-go-v2 v1.30.5 + github.com/aws/aws-sdk-go-v2 v1.32.2 github.com/aws/aws-sdk-go-v2/config v1.27.23 github.com/aws/aws-sdk-go-v2/credentials v1.17.23 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.4 - github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0 - github.com/aws/smithy-go v1.21.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0 + github.com/aws/smithy-go v1.22.0 github.com/go-resty/resty/v2 v2.14.0 github.com/google/go-cmp v0.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 @@ -20,17 +20,17 @@ require ( github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0 github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 - github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.14.0 github.com/hashicorp/terraform-plugin-go v0.24.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.16.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 github.com/hashicorp/terraform-plugin-testing v1.10.0 - github.com/linode/linodego v1.41.0 + github.com/linode/linodego v1.42.0 github.com/linode/linodego/k8s v1.25.2 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.27.0 - golang.org/x/net v0.29.0 + golang.org/x/crypto v0.28.0 + golang.org/x/net v0.30.0 golang.org/x/sync v0.8.0 ) @@ -38,16 +38,16 @@ require ( github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect @@ -102,9 +102,9 @@ require ( github.com/zclconf/go-cty v1.15.0 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 2f6d9a1df..1debe410d 100644 --- a/go.sum +++ b/go.sum @@ -9,10 +9,10 @@ github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= -github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw= +github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= +github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA= github.com/aws/aws-sdk-go-v2/config v1.27.23 h1:Cr/gJEa9NAS7CDAjbnB7tHYb3aLZI2gVggfmSAasDac= github.com/aws/aws-sdk-go-v2/config v1.27.23/go.mod h1:WMMYHqLCFu5LH05mFOF5tsq1PGEMfKbu083VKqLCd0o= github.com/aws/aws-sdk-go-v2/credentials v1.17.23 h1:G1CfmLVoO2TdQ8z9dW+JBc/r8+MqyPQhXCafNZcXVZo= @@ -21,32 +21,32 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 h1:Aznqksmd6Rfv2HQN9cpqIV/ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9/go.mod h1:WQr3MY7AxGNxaqAtsDWn+fBxmd4XvLkzeqQ8P1VM0/w= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.4 h1:6eKRM6fgeXG4krRO9XKz755vuRhT5UyB9M1W6vjA3JU= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.4/go.mod h1:h0TjcRi+nTob6fksqubKOe+Hra8uqfgmN+vuw4xRwWE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 h1:Roo69qTpfu8OlJ2Tb7pAYVuF0CpuUMB0IYWwYP/4DZM= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17/go.mod h1:NcWPxQzGM1USQggaTVwz6VpqMZPX1CvDJLDh6jnOCa4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 h1:FLMkfEiRjhgeDTCjjLoc3URo/TBkgeQbocA78lfkzSI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19/go.mod h1:Vx+GucNSsdhaxs3aZIKfSUjKVGsxN25nX2SRcdhuw08= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 h1:u+EfGmksnJc/x5tq3A+OD7LrMbSSR/5TrKLvkdy/fhY= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17/go.mod h1:VaMx6302JHax2vHJWgRo+5n9zvbacs3bLU/23DNQrTY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0 h1:rd/aA3iDq1q7YsL5sc4dEwChutH7OZF9Ihfst6pXQzI= -github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0/go.mod h1:5FmD/Dqq57gP+XwaUnd5WFPipAuzrf0HmupX27Gvjvc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 h1:7edmS3VOBDhK00b/MwGtGglCm7hhwNYnjJs/PgFdMQE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21/go.mod h1:Q9o5h4HoIWG8XfzxqiuK/CGUbepCJ8uTlaE3bAbxytQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 h1:4FMHqLfk0efmTqhXVRL5xYRqlEBNBiRI7N6w4jsEdd4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2/go.mod h1:LWoqeWlK9OZeJxsROW2RqrSPvQHKTpp69r/iDjwsSaw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 h1:t7iUP9+4wdc5lt3E41huP+GvQZJD38WLsgVp4iOtAjg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2/go.mod h1:/niFCtmuQNxqx9v8WAPq5qh7EH25U4BF6tjoyq9bObM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0 h1:xA6XhTF7PE89BCNHJbQi8VvPzcgMtmGC5dr8S8N7lHk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0/go.mod h1:cB6oAuus7YXRZhWCc1wIwPywwZ1XwweNp2TVAEGYeB8= github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 h1:p1GahKIjyMDZtiKoIn0/jAj/TkMzfzndDv5+zi2Mhgc= github.com/aws/aws-sdk-go-v2/service/sso v1.22.1/go.mod h1:/vWdhoIoYA5hYoPZ6fm7Sv4d8701PiG5VKe8/pPJL60= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.1 h1:lCEv9f8f+zJ8kcFeAjRZsekLd/x5SAm96Cva+VbUdo8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.1/go.mod h1:xyFHA4zGxgYkdD73VeezHt3vSKEG9EmFnGwoKlP00u4= github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 h1:+woJ607dllHJQtsnJLi52ycuqHMwlW+Wqm2Ppsfp4nQ= github.com/aws/aws-sdk-go-v2/service/sts v1.30.1/go.mod h1:jiNR3JqT15Dm+QWq2SRgh0x0bCNSRP2L25+CqPNpJlQ= -github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= -github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= @@ -147,8 +147,8 @@ github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaK github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 h1:v3DapR8gsp3EM8fKMh6up9cJUFQ2iRaFsYLP8UJnCco= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0/go.mod h1:c3PnGE9pHBDfdEVG9t1S1C9ia5LW+gkFR0CygXlM8ak= -github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E= -github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo= +github.com/hashicorp/terraform-plugin-framework-validators v0.14.0 h1:3PCn9iyzdVOgHYOBmncpSSOxjQhCTYmc+PGvbdlqSaI= +github.com/hashicorp/terraform-plugin-framework-validators v0.14.0/go.mod h1:LwDKNdzxrDY/mHBrlC6aYfE2fQ3Dk3gaJD64vNiXvo4= github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U= github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= @@ -189,8 +189,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/linode/linodego v1.41.0 h1:GcP7JIBr9iLRJ9FwAtb9/WCT1DuPJS/xUApapfdjtiY= -github.com/linode/linodego v1.41.0/go.mod h1:Ow4/XZ0yvWBzt3iAHwchvhSx30AyLintsSMvvQ2/SJY= +github.com/linode/linodego v1.42.0 h1:ZSbi4MtvwrfB9Y6bknesorvvueBGGilcmh2D5dq76RM= +github.com/linode/linodego v1.42.0/go.mod h1:2yzmY6pegPBDgx2HDllmt0eIk2IlzqcgK6NR0wFCFRY= github.com/linode/linodego/k8s v1.25.2 h1:PY6S0sAD3xANVvM9WY38bz9GqMTjIbytC8IJJ9Cv23o= github.com/linode/linodego/k8s v1.25.2/go.mod h1:DC1XCSRZRGsmaa/ggpDPSDUmOM6aK1bhSIP6+f9Cwhc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -273,8 +273,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -296,8 +296,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -331,8 +331,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -342,8 +342,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -355,8 +355,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/linode/childaccount/datasource_test.go b/linode/childaccount/datasource_test.go index b800ff486..3f0dac7c9 100644 --- a/linode/childaccount/datasource_test.go +++ b/linode/childaccount/datasource_test.go @@ -10,6 +10,19 @@ import ( "github.com/linode/terraform-provider-linode/v2/linode/childaccount/tmpl" ) +func TestSmokeTests_childaccount(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccDataSourceChildAccount_basic_smoke", TestAccDataSourceChildAccount_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccDataSourceChildAccount_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/databasepostgresql/resource_test.go b/linode/databasepostgresql/resource_test.go index afa66a1ec..518acd7d4 100644 --- a/linode/databasepostgresql/resource_test.go +++ b/linode/databasepostgresql/resource_test.go @@ -75,6 +75,19 @@ func sweep(prefix string) error { return nil } +func TestSmokeTests_databasepostgresql(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceDatabasePostgres_basic_smoke", TestAccResourceDatabasePostgres_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceDatabasePostgres_basic_smoke(t *testing.T) { acceptance.LongRunningTest(t) diff --git a/linode/domain/datasource_test.go b/linode/domain/datasource_test.go index 8a7f81f0f..2b54315d6 100644 --- a/linode/domain/datasource_test.go +++ b/linode/domain/datasource_test.go @@ -12,6 +12,19 @@ import ( "github.com/linode/terraform-provider-linode/v2/linode/domain/tmpl" ) +func TestSmokeTests_domain_datasource(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccDataSourceDomain_basic_smoke", TestAccDataSourceDomain_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccDataSourceDomain_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/domain/resource_test.go b/linode/domain/resource_test.go index b39a74d02..059939c67 100644 --- a/linode/domain/resource_test.go +++ b/linode/domain/resource_test.go @@ -48,6 +48,19 @@ func sweep(prefix string) error { return nil } +func TestSmokeTests_domain_resource(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceDomain_basic_smoke", TestAccResourceDomain_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceDomain_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/firewall/framework_resource_test.go b/linode/firewall/framework_resource_test.go index 26f126205..c309fb2be 100644 --- a/linode/firewall/framework_resource_test.go +++ b/linode/firewall/framework_resource_test.go @@ -57,6 +57,19 @@ func sweep(prefix string) error { return nil } +func TestSmokeTests_firewall(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccLinodeFirewall_basic", TestAccLinodeFirewall_basic}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccLinodeFirewall_basic(t *testing.T) { t.Parallel() @@ -105,7 +118,7 @@ func TestAccLinodeFirewall_basic(t *testing.T) { ResourceName: testFirewallResName, ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"created"}, + ImportStateVerifyIgnore: []string{"created", "updated"}, }, }, }) @@ -141,7 +154,7 @@ func TestAccLinodeFirewall_minimum(t *testing.T) { ResourceName: testFirewallResName, ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"created"}, + ImportStateVerifyIgnore: []string{"created", "updated"}, }, }, }) @@ -212,7 +225,7 @@ func TestAccLinodeFirewall_multipleRules(t *testing.T) { ResourceName: testFirewallResName, ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"created"}, + ImportStateVerifyIgnore: []string{"created", "updated"}, }, }, }) @@ -250,7 +263,7 @@ func TestAccLinodeFirewall_no_device(t *testing.T) { ResourceName: testFirewallResName, ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"created"}, + ImportStateVerifyIgnore: []string{"created", "updated"}, }, }, }) @@ -449,9 +462,10 @@ func TestAccLinodeFirewall_noIPv6(t *testing.T) { ), }, { - ResourceName: testFirewallResName, - ImportState: true, - ImportStateVerify: true, + ResourceName: testFirewallResName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"created", "updated"}, }, }, }) @@ -483,7 +497,7 @@ func TestAccLinodeFirewall_noRules(t *testing.T) { ResourceName: testFirewallResName, ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"created"}, + ImportStateVerifyIgnore: []string{"created", "updated"}, }, }, }) diff --git a/linode/firewalldevice/resource_test.go b/linode/firewalldevice/resource_test.go index e595db960..0ac2dacea 100644 --- a/linode/firewalldevice/resource_test.go +++ b/linode/firewalldevice/resource_test.go @@ -29,6 +29,19 @@ func init() { testRegion = region } +func TestSmokeTests_firewalldevice(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceFirewallDevice_basic_smoke", TestAccResourceFirewallDevice_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceFirewallDevice_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/framework_provider.go b/linode/framework_provider.go index 22cab83ff..30d0c1810 100644 --- a/linode/framework_provider.go +++ b/linode/framework_provider.go @@ -3,8 +3,6 @@ package linode import ( "context" - "github.com/linode/terraform-provider-linode/v2/linode/vpcips" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -46,13 +44,16 @@ import ( "github.com/linode/terraform-provider-linode/v2/linode/lke" "github.com/linode/terraform-provider-linode/v2/linode/lkeclusters" "github.com/linode/terraform-provider-linode/v2/linode/lkenodepool" + "github.com/linode/terraform-provider-linode/v2/linode/lketypes" "github.com/linode/terraform-provider-linode/v2/linode/lkeversions" "github.com/linode/terraform-provider-linode/v2/linode/nb" "github.com/linode/terraform-provider-linode/v2/linode/nbconfig" "github.com/linode/terraform-provider-linode/v2/linode/nbconfigs" "github.com/linode/terraform-provider-linode/v2/linode/nbnode" "github.com/linode/terraform-provider-linode/v2/linode/nbs" + "github.com/linode/terraform-provider-linode/v2/linode/nbtypes" "github.com/linode/terraform-provider-linode/v2/linode/networkingip" + "github.com/linode/terraform-provider-linode/v2/linode/networktransferprices" "github.com/linode/terraform-provider-linode/v2/linode/objbucket" "github.com/linode/terraform-provider-linode/v2/linode/objcluster" "github.com/linode/terraform-provider-linode/v2/linode/objkey" @@ -73,7 +74,9 @@ import ( "github.com/linode/terraform-provider-linode/v2/linode/vlan" "github.com/linode/terraform-provider-linode/v2/linode/volume" "github.com/linode/terraform-provider-linode/v2/linode/volumes" + "github.com/linode/terraform-provider-linode/v2/linode/volumetypes" "github.com/linode/terraform-provider-linode/v2/linode/vpc" + "github.com/linode/terraform-provider-linode/v2/linode/vpcips" "github.com/linode/terraform-provider-linode/v2/linode/vpcs" "github.com/linode/terraform-provider-linode/v2/linode/vpcsubnet" "github.com/linode/terraform-provider-linode/v2/linode/vpcsubnets" @@ -140,6 +143,10 @@ func (p *FrameworkProvider) Schema( Optional: true, Description: "The version of Linode API.", }, + "api_ca_path": schema.StringAttribute{ + Optional: true, + Description: "The path to a Linode API CA file to trust.", + }, "skip_instance_ready_poll": schema.BoolAttribute{ Optional: true, Description: "Skip waiting for a linode_instance resource to be running.", @@ -237,6 +244,7 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource profile.NewDataSource, nb.NewDataSource, networkingip.NewDataSource, + networktransferprices.NewDataSource, lkeversions.NewDataSource, regions.NewDataSource, ipv6range.NewDataSource, @@ -253,6 +261,7 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource domain.NewDataSource, user.NewDataSource, nbconfig.NewDataSource, + nbtypes.NewDataSource, instancetype.NewDataSource, instancetypes.NewDataSource, image.NewDataSource, @@ -276,12 +285,14 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource vpcsubnets.NewDataSource, vpcs.NewDataSource, volumes.NewDataSource, + volumetypes.NewDataSource, accountavailability.NewDataSource, nbconfigs.NewDataSource, ipv6ranges.NewDataSource, domains.NewDataSource, lke.NewDataSource, lkeclusters.NewDataSource, + lketypes.NewDataSource, placementgroup.NewDataSource, placementgroups.NewDataSource, childaccount.NewDataSource, diff --git a/linode/framework_provider_config.go b/linode/framework_provider_config.go index 0b58e6e55..e9d3f115f 100644 --- a/linode/framework_provider_config.go +++ b/linode/framework_provider_config.go @@ -157,6 +157,13 @@ func (fp *FrameworkProvider) HandleDefaults( ) } + if lpm.APICAPath.IsNull() { + lpm.APICAPath = GetStringFromEnv( + linodego.APIHostCert, + types.StringNull(), + ) + } + if lpm.SkipInstanceReadyPoll.IsNull() { lpm.SkipInstanceReadyPoll = types.BoolValue(false) } @@ -229,17 +236,6 @@ func (fp *FrameworkProvider) InitProvider( return } - loggingTransport := helper.NewAPILoggerTransport( - logging.NewSubsystemLoggingHTTPTransport( - helper.APILoggerSubsystem, - http.DefaultTransport, - ), - ) - - oauth2Client := &http.Client{ - Transport: loggingTransport, - } - accessToken := lpm.AccessToken.ValueString() APIURL := lpm.APIURL.ValueString() APIVersion := lpm.APIVersion.ValueString() @@ -259,14 +255,44 @@ func (fp *FrameworkProvider) InitProvider( eventPollMilliseconds := lpm.EventPollMilliseconds.ValueInt64() // LKENodeReadyPollMilliseconds := lpm.LKEEventPollMilliseconds.ValueInt64() + httpTransport := http.DefaultTransport.(*http.Transport).Clone() + + if !lpm.APICAPath.IsNull() { + caPath, err := helper.ExpandPath(lpm.APICAPath.ValueString()) + if err != nil { + diags.AddError("Failed to expand api_ca_path", err.Error()) + return + } + + if err := helper.AddRootCAToTransport(caPath, httpTransport); err != nil { + diags.AddError("Failed to add root CA to HTTP transport", err.Error()) + return + } + } + + oauth2Client := &http.Client{ + Transport: helper.NewAPILoggerTransport( + logging.NewSubsystemLoggingHTTPTransport( + helper.APILoggerSubsystem, + httpTransport, + ), + ), + } + client := linodego.NewClient(oauth2Client) // Load the config file if it exists if _, err := os.Stat(configPath); err == nil { tflog.Info(ctx, "Using Linode profile", map[string]any{ "config_path": lpm.ConfigPath, }) + + configPathExpanded, err := helper.ExpandPath(configPath) + if err != nil { + diags.AddError("Failed to expand config path", err.Error()) + } + err = client.LoadConfig(&linodego.LoadConfigOptions{ - Path: configPath, + Path: configPathExpanded, Profile: configProfile, }) if err != nil { diff --git a/linode/helper/base_pricing_types.go b/linode/helper/base_pricing_types.go new file mode 100644 index 000000000..f3e96878c --- /dev/null +++ b/linode/helper/base_pricing_types.go @@ -0,0 +1,50 @@ +package helper + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var PriceObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "hourly": types.Float64Type, + "monthly": types.Float64Type, + }, +} + +var RegionPriceObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "hourly": types.Float64Type, + "monthly": types.Float64Type, + }, +} + +func GetPricingTypeAttributes(typeName string) map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique ID assigned to this " + typeName + ".", + Required: true, + }, + "label": schema.StringAttribute{ + Description: "The " + typeName + "'s label.", + Computed: true, + Optional: true, + }, + "price": schema.ListAttribute{ + Description: "Cost in US dollars, broken down into hourly and monthly charges.", + Computed: true, + ElementType: PriceObjectType, + }, + "region_prices": schema.ListAttribute{ + Description: "A list of region-specific prices for this " + typeName + ".", + Computed: true, + ElementType: RegionPriceObjectType, + }, + "transfer": schema.Int64Attribute{ + Description: "The monthly outbound transfer amount, in MB.", + Computed: true, + }, + } +} diff --git a/linode/helper/compare.go b/linode/helper/compare.go index 172a45c79..292de1636 100644 --- a/linode/helper/compare.go +++ b/linode/helper/compare.go @@ -1,7 +1,10 @@ package helper import ( + "fmt" + "reflect" "slices" + "sort" "strings" "time" ) @@ -72,7 +75,7 @@ func ValidateSubset(superset, subset []any) bool { // Check if two slices are equivalent without considering ordering, // assuming no duplicated items are in the sets. func CompareSets(a, b []any) bool { - return ValidateSubset(a, b) && ValidateSubset(b, a) + return CompareSlices(true, true, a, b) } // Check if two string slices are equivalent without considering ordering, @@ -95,3 +98,19 @@ func CompareScopes(s1, s2 string) bool { s2List := strings.Split(s2, " ") return StringListElementsEqual(s1List, s2List) } + +func CompareSlices(ignoreNil, unordered bool, a, b []any) bool { + if ignoreNil && len(a) == 0 && len(b) == 0 { + return true + } + + if unordered { + less := func(i, j int) bool { + return fmt.Sprintf("%v", a[i]) < fmt.Sprintf("%v", a[j]) + } + sort.Slice(a, less) + sort.Slice(b, less) + } + + return reflect.DeepEqual(a, b) +} diff --git a/linode/helper/config.go b/linode/helper/config.go index c803744ad..9f79e4755 100644 --- a/linode/helper/config.go +++ b/linode/helper/config.go @@ -30,6 +30,7 @@ type Config struct { AccessToken string APIURL string APIVersion string + APICAPath string UAPrefix string ConfigPath string @@ -55,15 +56,26 @@ type Config struct { // Client returns a fully initialized Linode client. func (c *Config) Client(ctx context.Context) (*linodego.Client, error) { - loggingTransport := NewAPILoggerTransport( - logging.NewSubsystemLoggingHTTPTransport( - APILoggerSubsystem, - http.DefaultTransport, - ), - ) + httpTransport := http.DefaultTransport.(*http.Transport).Clone() + + if c.APICAPath != "" { + caPath, err := ExpandPath(c.APICAPath) + if err != nil { + return nil, fmt.Errorf("failed to expand api_ca_path: %w", err) + } + + if err := AddRootCAToTransport(caPath, httpTransport); err != nil { + return nil, fmt.Errorf("failed to add root CA %s to HTTP transport: %w", c.APICAPath, err) + } + } oauth2Client := &http.Client{ - Transport: loggingTransport, + Transport: NewAPILoggerTransport( + logging.NewSubsystemLoggingHTTPTransport( + APILoggerSubsystem, + httpTransport, + ), + ), } client := linodego.NewClient(oauth2Client) @@ -72,11 +84,16 @@ func (c *Config) Client(ctx context.Context) (*linodego.Client, error) { // Load the config file if it exists if _, err := os.Stat(c.ConfigPath); err == nil { + configPath, err := ExpandPath(c.ConfigPath) + if err != nil { + return nil, fmt.Errorf("failed to expand config path: %w", err) + } + tflog.Info(ctx, "Using Linode profile", map[string]any{ "config_path": c.ConfigPath, }) err = client.LoadConfig(&linodego.LoadConfigOptions{ - Path: c.ConfigPath, + Path: configPath, Profile: c.ConfigProfile, }) if err != nil { diff --git a/linode/helper/conversion.go b/linode/helper/conversion.go index 9a65607ac..0e90dff3c 100644 --- a/linode/helper/conversion.go +++ b/linode/helper/conversion.go @@ -29,6 +29,26 @@ func AnySliceToTyped[T any](obj []any) []T { return result } +func StringTypedMapToAny[T any](m map[string]T) map[string]any { + result := make(map[string]any, len(m)) + + for k, v := range m { + result[k] = v + } + + return result +} + +func StringAnyMapToTyped[T any](m map[string]any) map[string]T { + result := make(map[string]T, len(m)) + + for k, v := range m { + result[k] = v.(T) + } + + return result +} + func StringAliasSliceToStringSlice[T ~string](obj []T) ([]string, error) { var result []string diff --git a/linode/helper/expand.go b/linode/helper/expand.go index b2d9ffe4d..bd0d40bf3 100644 --- a/linode/helper/expand.go +++ b/linode/helper/expand.go @@ -22,7 +22,21 @@ func ExpandStringSet(set *schema.Set) []string { return ExpandStringList(set.List()) } -func ExpandIntList(list []interface{}) []int { +func ExpandObjectList(list []any) []map[string]any { + slice := make([]map[string]any, 0, len(list)) + for _, s := range list { + if val, ok := s.(map[string]any); ok { + slice = append(slice, val) + } + } + return slice +} + +func ExpandObjectSet(set *schema.Set) []map[string]any { + return ExpandObjectList(set.List()) +} + +func ExpandIntList(list []any) []int { slice := make([]int, 0, len(list)) for _, n := range list { if val, ok := n.(int); ok { diff --git a/linode/helper/filepath.go b/linode/helper/filepath.go new file mode 100644 index 000000000..aa73efeb3 --- /dev/null +++ b/linode/helper/filepath.go @@ -0,0 +1,29 @@ +package helper + +import ( + "fmt" + "os" + "strings" +) + +const pathSeparatorString = string(os.PathSeparator) + +// ExpandPath expands the given path, replacing ~'s with the user's +// home directory. +// NOTE: This does not implement feature-complete tilde expansion. +func ExpandPath(path string) (string, error) { + segments := strings.Split(path, pathSeparatorString) + + if segments[0] == "~" { + homePath, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("could not expand home path: %w", err) + } + + segments[0] = homePath + } + + // We don't use filepath.Join(...) here because it does not + // support rebuilding paths starting with `/`. + return strings.Join(segments, pathSeparatorString), nil +} diff --git a/linode/helper/filepath_test.go b/linode/helper/filepath_test.go new file mode 100644 index 000000000..2c6fc1697 --- /dev/null +++ b/linode/helper/filepath_test.go @@ -0,0 +1,32 @@ +//go:build unit + +package helper + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExpandPath(t *testing.T) { + homePath, err := os.UserHomeDir() + require.NoError(t, err) + + expandedPath, err := ExpandPath(filepath.Join("~", "foo", "bar")) + require.NoError(t, err) + require.Equal(t, filepath.Join(homePath, "foo", "bar"), expandedPath) + + expandedPath, err = ExpandPath("") + require.NoError(t, err) + require.Equal(t, "", expandedPath) + + // /foo/bar + absPath := strings.Join([]string{"", "foo", "bar"}, string(os.PathSeparator)) + + expandedPath, err = ExpandPath(absPath) + require.NoError(t, err) + require.Equal(t, absPath, expandedPath) +} diff --git a/linode/helper/framework_provider_model.go b/linode/helper/framework_provider_model.go index 9daf87274..36c6ea6e4 100644 --- a/linode/helper/framework_provider_model.go +++ b/linode/helper/framework_provider_model.go @@ -11,6 +11,7 @@ func GetFrameworkProviderModelFromSDKv2ProviderConfig(config *Config) *Framework AccessToken: types.StringValue(config.AccessToken), APIURL: types.StringValue(config.APIURL), APIVersion: types.StringValue(config.APIVersion), + APICAPath: types.StringValue(config.APICAPath), UAPrefix: types.StringValue(config.UAPrefix), ConfigPath: types.StringValue(config.ConfigPath), ConfigProfile: types.StringValue(config.ConfigProfile), @@ -34,6 +35,7 @@ type FrameworkProviderModel struct { AccessToken types.String `tfsdk:"token"` APIURL types.String `tfsdk:"url"` APIVersion types.String `tfsdk:"api_version"` + APICAPath types.String `tfsdk:"api_ca_path"` UAPrefix types.String `tfsdk:"ua_prefix"` ConfigPath types.String `tfsdk:"config_path"` diff --git a/linode/helper/http.go b/linode/helper/http.go new file mode 100644 index 000000000..4ca3e4e5e --- /dev/null +++ b/linode/helper/http.go @@ -0,0 +1,33 @@ +package helper + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "os" + "path/filepath" +) + +// AddRootCAToTransport applies the cert file at the given path to the given *http.Transport +func AddRootCAToTransport(cert string, transport *http.Transport) error { + certData, err := os.ReadFile(filepath.Clean(cert)) + if err != nil { + return fmt.Errorf("failed to read cert file %s: %w", cert, err) + } + + tlsConfig := transport.TLSClientConfig + if tlsConfig == nil { + tlsConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + } + + if tlsConfig.RootCAs == nil { + tlsConfig.RootCAs = x509.NewCertPool() + } + + tlsConfig.RootCAs.AppendCertsFromPEM(certData) + + return nil +} diff --git a/linode/image/resource_test.go b/linode/image/resource_test.go index 8184c5d7b..f40af614b 100644 --- a/linode/image/resource_test.go +++ b/linode/image/resource_test.go @@ -36,8 +36,10 @@ var testImageBytesNew = []byte{ 0xe4, 0x02, 0x00, 0x7a, 0x7a, 0x6f, 0xed, 0x03, 0x00, 0x00, 0x00, } -var testRegion string -var testRegions []string +var ( + testRegion string + testRegions []string +) func init() { resource.AddTestSweepers("linode_image", &resource.Sweeper{ diff --git a/linode/instance/datasource_test.go b/linode/instance/datasource_test.go index 3e1b879da..07dfcb105 100644 --- a/linode/instance/datasource_test.go +++ b/linode/instance/datasource_test.go @@ -50,6 +50,41 @@ func TestAccDataSourceInstances_basic(t *testing.T) { }) } +func TestAccDataSourceInstances_withBlockStorageEncryption(t *testing.T) { + t.Parallel() + + resName := "data.linode_instances.foobar" + instanceName := acctest.RandomWithPrefix("tf_test") + + // Resolve a region with support for Block Storage Encryption + targetRegion, err := acceptance.GetRandomRegionWithCaps( + []string{"Linodes", "Block Storage Encryption"}, + "core", + ) + if err != nil { + t.Fatal(err) + } + + rootPass := acctest.RandString(64) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: acceptance.CheckInstanceDestroy, + + Steps: []resource.TestStep{ + { + Config: tmpl.DataWithBlockStorageEncryption(t, instanceName, targetRegion, rootPass), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resName, "instances.#", "1"), + resource.TestCheckResourceAttrSet(resName, "instances.0.id"), + resource.TestCheckTypeSetElemAttr(resName, "instances.0.capabilities.*", "Block Storage Encryption"), + ), + }, + }, + }) +} + func TestAccDataSourceInstances_withPG(t *testing.T) { t.Parallel() diff --git a/linode/instance/flatten.go b/linode/instance/flatten.go index b3566bf07..adf6decd8 100644 --- a/linode/instance/flatten.go +++ b/linode/instance/flatten.go @@ -46,6 +46,7 @@ func flattenInstance( result["watchdog_enabled"] = instance.WatchdogEnabled result["group"] = instance.Group result["tags"] = instance.Tags + result["capabilities"] = instance.Capabilities result["image"] = instance.Image result["host_uuid"] = instance.HostUUID result["has_user_data"] = instance.HasUserData @@ -215,6 +216,7 @@ func flattenInstanceSimple(instance *linodego.Instance) (map[string]interface{}, result["watchdog_enabled"] = instance.WatchdogEnabled result["group"] = instance.Group result["tags"] = instance.Tags + result["capabilities"] = instance.Capabilities result["image"] = instance.Image result["host_uuid"] = instance.HostUUID result["backups"] = flattenInstanceBackups(*instance) diff --git a/linode/instance/resource.go b/linode/instance/resource.go index 76c6c240f..3b688b2a0 100644 --- a/linode/instance/resource.go +++ b/linode/instance/resource.go @@ -112,6 +112,7 @@ func readResource(ctx context.Context, d *schema.ResourceData, meta interface{}) d.Set("watchdog_enabled", instance.WatchdogEnabled) d.Set("group", instance.Group) d.Set("tags", instance.Tags) + d.Set("capabilities", instance.Capabilities) d.Set("booted", isInstanceBooted(instance)) d.Set("host_uuid", instance.HostUUID) d.Set("has_user_data", instance.HasUserData) @@ -137,6 +138,8 @@ func readResource(ctx context.Context, d *schema.ResourceData, meta interface{}) placementGroupMap["compliant_only"] = compliantOnly.(bool) } d.Set("placement_group", []map[string]interface{}{placementGroupMap}) + } else { + d.Set("placement_group", nil) } disks, swapSize := flattenInstanceDisks(instanceDisks) diff --git a/linode/instance/resource_test.go b/linode/instance/resource_test.go index b3f9c702b..db0d77e16 100644 --- a/linode/instance/resource_test.go +++ b/linode/instance/resource_test.go @@ -64,6 +64,19 @@ func sweep(prefix string) error { return nil } +func TestSmokeTests_instance(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceInstance_basic_smoke", TestAccResourceInstance_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceInstance_basic_smoke(t *testing.T) { t.Parallel() @@ -504,7 +517,6 @@ func TestAccResourceInstance_disk(t *testing.T) { checkComputeInstanceDisk(&instance, "disk", 3000), ), }, - { ResourceName: resName, ImportState: true, diff --git a/linode/instance/schema_datasource.go b/linode/instance/schema_datasource.go index 66e344901..b762529db 100644 --- a/linode/instance/schema_datasource.go +++ b/linode/instance/schema_datasource.go @@ -33,6 +33,12 @@ var instanceDataSourceSchema = map[string]*schema.Schema{ Elem: &schema.Schema{Type: schema.TypeString}, Computed: true, }, + "capabilities": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Description: "A list of capabilities of this Linode instance.", + }, "boot_config_label": { Type: schema.TypeString, Description: "The Label of the Instance Config that should be used to boot the Linode instance.", diff --git a/linode/instance/schema_resource.go b/linode/instance/schema_resource.go index cb65f77ae..70e12c386 100644 --- a/linode/instance/schema_resource.go +++ b/linode/instance/schema_resource.go @@ -267,6 +267,12 @@ var resourceSchema = map[string]*schema.Schema{ Description: "An array of tags applied to this object. Tags are for organizational purposes only.", Computed: true, }, + "capabilities": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Description: "A list of capabilities of this Linode instance.", + }, "boot_config_label": { Type: schema.TypeString, Description: "The Label of the Instance Config that should be used to boot the Linode instance.", diff --git a/linode/instance/tmpl/template.go b/linode/instance/tmpl/template.go index 2dd51a57d..ef236d0fb 100644 --- a/linode/instance/tmpl/template.go +++ b/linode/instance/tmpl/template.go @@ -623,6 +623,15 @@ func DataBasic(t *testing.T, label, region string, rootPass string) string { }) } +func DataWithBlockStorageEncryption(t *testing.T, label, region string, rootPass string) string { + return acceptance.ExecuteTemplate(t, + "instance_data_with_block_storage_encryption", TemplateData{ + Label: label, + Region: region, + RootPass: rootPass, + }) +} + func DataWithPG(t *testing.T, label, region, assignedGroup string, groups []string) string { return acceptance.ExecuteTemplate(t, "instance_data_with_pg", TemplateData{ diff --git a/linode/instance/tmpl/templates/data_with_block_storage_encryption.gotf b/linode/instance/tmpl/templates/data_with_block_storage_encryption.gotf new file mode 100644 index 000000000..75efd8043 --- /dev/null +++ b/linode/instance/tmpl/templates/data_with_block_storage_encryption.gotf @@ -0,0 +1,20 @@ +{{ define "instance_data_with_block_storage_encryption" }} + +{{ template "e2e_test_firewall" . }} + +resource "linode_instance" "foobar" { + label = "{{.Label}}" + type = "g6-nanode-1" + region = "{{ .Region }}" + root_pass = "{{ .RootPass }}" + firewall_id = linode_firewall.e2e_test_firewall.id +} + +data "linode_instances" "foobar" { + filter { + name = "id" + values = [linode_instance.foobar.id] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/instancedisk/framework_resource.go b/linode/instancedisk/framework_resource.go index e357ca9ee..6cac26e33 100644 --- a/linode/instancedisk/framework_resource.go +++ b/linode/instancedisk/framework_resource.go @@ -119,34 +119,35 @@ func (r *Resource) Create( return } + diskID := disk.ID // Add resource to TF states earlier to prevent // dangling resources (resources created but not managed by TF) AddDiskResource(ctx, *disk, resp, plan) - ctx = tflog.SetField(ctx, "disk_id", disk.ID) + ctx = tflog.SetField(ctx, "disk_id", diskID) _, err = p.WaitForFinished(ctx, timeoutSeconds) if err != nil { resp.Diagnostics.AddError( - fmt.Sprintf("Failed to Wait for the Disk Creation Event on Linode Disk (%d)", disk.ID), + fmt.Sprintf("Failed to Wait for the Disk Creation Event on Linode Disk (%d)", diskID), err.Error(), ) } if _, err := client.WaitForInstanceDiskStatus( - ctx, linodeID, disk.ID, linodego.DiskReady, timeoutSeconds, + ctx, linodeID, diskID, linodego.DiskReady, timeoutSeconds, ); err != nil { resp.Diagnostics.AddError( - fmt.Sprintf("Failed to Wait for Disk (%d) to be Ready", disk.ID), err.Error(), + fmt.Sprintf("Failed to Wait for Disk (%d) to be Ready", diskID), err.Error(), ) } // get latest status of the disk tflog.Trace(ctx, "client.GetInstanceDisk(...)") - disk, err = client.GetInstanceDisk(ctx, linodeID, disk.ID) + disk, err = client.GetInstanceDisk(ctx, linodeID, diskID) if err != nil { resp.Diagnostics.AddError( - fmt.Sprintf("Failed to Get Disk %d of Linode Instance %d", disk.ID, linodeID), + fmt.Sprintf("Failed to Get Disk %d of Linode Instance %d", diskID, linodeID), err.Error(), ) } diff --git a/linode/instancedisk/resource_test.go b/linode/instancedisk/resource_test.go index 3755e8bd4..7d79b5688 100644 --- a/linode/instancedisk/resource_test.go +++ b/linode/instancedisk/resource_test.go @@ -29,6 +29,19 @@ func init() { testRegion = region } +func TestSmokeTests_instancedisk(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceInstanceDisk_basic_smoke", TestAccResourceInstanceDisk_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceInstanceDisk_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/instancetype/framework_datasource_schema.go b/linode/instancetype/framework_datasource_schema.go index 8af4219bf..351808cff 100644 --- a/linode/instancetype/framework_datasource_schema.go +++ b/linode/instancetype/framework_datasource_schema.go @@ -4,27 +4,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/terraform-provider-linode/v2/linode/helper" ) -var priceObjectType = types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "hourly": types.Float64Type, - "monthly": types.Float64Type, - }, -} - -var regionPriceObjectType = types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "id": types.StringType, - "hourly": types.Float64Type, - "monthly": types.Float64Type, - }, -} - var backupsObjectType = types.ObjectType{ AttrTypes: map[string]attr.Type{ - "price": types.ListType{ElemType: priceObjectType}, - "region_prices": types.ListType{ElemType: regionPriceObjectType}, + "price": types.ListType{ElemType: helper.PriceObjectType}, + "region_prices": types.ListType{ElemType: helper.RegionPriceObjectType}, }, } @@ -56,7 +42,7 @@ var Attributes = map[string]schema.Attribute{ "price": schema.ListAttribute{ Description: "Cost in US dollars, broken down into hourly and monthly charges.", Computed: true, - ElementType: priceObjectType, + ElementType: helper.PriceObjectType, }, "addons": schema.ListAttribute{ Description: "Information about the optional Backup service offered for Linodes.", @@ -66,7 +52,7 @@ var Attributes = map[string]schema.Attribute{ "region_prices": schema.ListAttribute{ Description: "A list of region-specific prices for this plan.", Computed: true, - ElementType: regionPriceObjectType, + ElementType: helper.RegionPriceObjectType, }, "network_out": schema.Int64Attribute{ Description: "The Mbits outbound bandwidth allocation.", diff --git a/linode/instancetype/framework_models.go b/linode/instancetype/framework_models.go index 83ded6d18..810e0c3e4 100644 --- a/linode/instancetype/framework_models.go +++ b/linode/instancetype/framework_models.go @@ -3,6 +3,8 @@ package instancetype import ( "context" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" @@ -134,7 +136,7 @@ func FlattenPrice(ctx context.Context, price linodego.LinodePrice) ( result["hourly"] = types.Float64Value(float64(price.Hourly)) result["monthly"] = types.Float64Value(float64(price.Monthly)) - obj, diag := types.ObjectValue(priceObjectType.AttrTypes, result) + obj, diag := types.ObjectValue(helper.PriceObjectType.AttrTypes, result) if diag.HasError() { return nil, diag } @@ -142,7 +144,7 @@ func FlattenPrice(ctx context.Context, price linodego.LinodePrice) ( objList := []attr.Value{obj} resultList, diag := types.ListValue( - priceObjectType, + helper.PriceObjectType, objList, ) if diag.HasError() { @@ -158,7 +160,7 @@ func FlattenRegionPrices(prices []linodego.LinodeRegionPrice) ( result := make([]attr.Value, len(prices)) for i, price := range prices { - obj, d := types.ObjectValue(regionPriceObjectType.AttrTypes, map[string]attr.Value{ + obj, d := types.ObjectValue(helper.RegionPriceObjectType.AttrTypes, map[string]attr.Value{ "id": types.StringValue(price.ID), "hourly": types.Float64Value(float64(price.Hourly)), "monthly": types.Float64Value(float64(price.Monthly)), @@ -171,7 +173,7 @@ func FlattenRegionPrices(prices []linodego.LinodeRegionPrice) ( } priceList, d := basetypes.NewListValue( - regionPriceObjectType, + helper.RegionPriceObjectType, result, ) return &priceList, d diff --git a/linode/lke/cluster.go b/linode/lke/cluster.go index 7af29b364..633d5bb4b 100644 --- a/linode/lke/cluster.go +++ b/linode/lke/cluster.go @@ -18,6 +18,8 @@ type NodePoolSpec struct { Tags []string Type string Count int + Taints []map[string]any + Labels map[string]string AutoScalerEnabled bool AutoScalerMin int AutoScalerMax int @@ -30,7 +32,7 @@ type NodePoolUpdates struct { } func ReconcileLKENodePoolSpecs( - oldSpecs []NodePoolSpec, newSpecs []NodePoolSpec, + ctx context.Context, oldSpecs []NodePoolSpec, newSpecs []NodePoolSpec, ) (NodePoolUpdates, error) { result := NodePoolUpdates{ ToCreate: make([]linodego.LKENodePoolCreateOptions, 0), @@ -40,9 +42,14 @@ func ReconcileLKENodePoolSpecs( createPool := func(spec NodePoolSpec) error { createOpts := linodego.LKENodePoolCreateOptions{ - Count: spec.Count, - Type: spec.Type, - Tags: spec.Tags, + Count: spec.Count, + Type: spec.Type, + Tags: spec.Tags, + Labels: linodego.LKENodePoolLabels(spec.Labels), + } + + if spec.Taints != nil { + createOpts.Taints = expandNodePoolTaints(spec.Taints) } if createOpts.Count == 0 { @@ -115,6 +122,16 @@ func ReconcileLKENodePoolSpecs( Tags: &newSpecs[i].Tags, } + if !helper.CompareSets(helper.TypedSliceToAny(newSpec.Taints), helper.TypedSliceToAny(oldSpec.Taints)) { + taints := expandNodePoolTaints(newSpec.Taints) + updateOpts.Taints = &taints + } + + if !reflect.DeepEqual(newSpec.Labels, oldSpec.Labels) && !(len(newSpec.Labels) == 0 && len(oldSpec.Labels) == 0) { + labels := linodego.LKENodePoolLabels(newSpecs[i].Labels) + updateOpts.Labels = &labels + } + // Only include the autoscaler if the autoscaler has updated // This isn't stricly necessary but it makes unit testing easier if newSpec.AutoScalerEnabled != oldSpec.AutoScalerEnabled || @@ -265,7 +282,8 @@ func recycleLKECluster(ctx context.Context, meta *helper.ProviderMeta, id int, p // This cannot currently be handled efficiently by a DiffSuppressFunc // See: https://github.com/hashicorp/terraform-plugin-sdk/issues/477 -func matchPoolsWithSchema(pools []linodego.LKENodePool, declaredPools []interface{}) ([]linodego.LKENodePool, error) { +func matchPoolsWithSchema(ctx context.Context, pools []linodego.LKENodePool, declaredPools []interface{}) ([]linodego.LKENodePool, error) { + tflog.Info(ctx, "Enter matchPoolsWithSchema helper function") result := make([]linodego.LKENodePool, len(declaredPools)) // Contains all unpaired pools returned by the API @@ -342,6 +360,21 @@ func matchPoolsWithSchema(pools []linodego.LKENodePool, declaredPools []interfac continue } + declaredTaints := expandNodePoolTaints(helper.ExpandObjectSet(declaredPool["taint"].(*schema.Set))) + + if !helper.CompareSets(helper.TypedSliceToAny(declaredTaints), helper.TypedSliceToAny(apiPool.Taints)) { + continue + } + + declaredLabels := helper.StringAnyMapToTyped[string](declaredPool["labels"].(map[string]any)) + + // - Length comparison is for handling the case of nil vs empty slice + // - Converting `apiPool.Labels` back to original (non-alias) type to make `reflect.DeepEqual` to really compare them + if !reflect.DeepEqual(declaredLabels, map[string]string(apiPool.Labels)) && + !(len(declaredLabels) == 0 && len(apiPool.Labels) == 0) { + continue + } + // Pair the API pool with the declared pool result[i] = apiPool delete(apiPools, apiPool.ID) @@ -395,6 +428,8 @@ func expandLinodeLKENodePoolSpecs(pool []interface{}, preserveNoTarget bool) (po ID: specMap["id"].(int), Type: specMap["type"].(string), Tags: helper.ExpandStringSet(specMap["tags"].(*schema.Set)), + Taints: helper.ExpandObjectSet(specMap["taint"].(*schema.Set)), + Labels: helper.StringAnyMapToTyped[string](specMap["labels"].(map[string]any)), Count: specMap["count"].(int), AutoScalerEnabled: autoscaler.Enabled, AutoScalerMin: autoscaler.Min, @@ -434,6 +469,8 @@ func flattenLKENodePools(pools []linodego.LKENodePool) []map[string]interface{} "type": pool.Type, "tags": pool.Tags, "disk_encryption": pool.DiskEncryption, + "taint": flattenNodePoolTaints(pool.Taints), + "labels": pool.Labels, "nodes": nodes, "autoscaler": autoscaler, } @@ -441,6 +478,20 @@ func flattenLKENodePools(pools []linodego.LKENodePool) []map[string]interface{} return flattened } +func flattenNodePoolTaints(taints []linodego.LKENodePoolTaint) []map[string]string { + result := make([]map[string]string, len(taints)) + + for i, t := range taints { + result[i] = map[string]string{ + "effect": string(t.Effect), + "key": t.Key, + "value": t.Value, + } + } + + return result +} + func flattenLKEClusterControlPlane(controlPlane linodego.LKEClusterControlPlane, aclResp *linodego.LKEClusterControlPlaneACLResponse) map[string]interface{} { flattened := make(map[string]interface{}) if aclResp != nil { @@ -549,3 +600,15 @@ func poolHasAnyOfTags(pool linodego.LKENodePool, tagSet map[string]bool) *string } return nil } + +func expandNodePoolTaints(poolTaints []map[string]any) []linodego.LKENodePoolTaint { + taints := make([]linodego.LKENodePoolTaint, len(poolTaints)) + for i, taint := range poolTaints { + taints[i] = linodego.LKENodePoolTaint{ + Key: taint["key"].(string), + Value: taint["value"].(string), + Effect: linodego.LKENodePoolTaintEffect(taint["effect"].(string)), + } + } + return taints +} diff --git a/linode/lke/cluster_test.go b/linode/lke/cluster_test.go index 4e1d93200..6f6cbf1fd 100644 --- a/linode/lke/cluster_test.go +++ b/linode/lke/cluster_test.go @@ -3,6 +3,7 @@ package lke_test import ( + "context" "reflect" "testing" @@ -145,7 +146,7 @@ func TestReconcileLKENodePoolSpecs(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - updates, err := lke.ReconcileLKENodePoolSpecs(tc.oldSpecs, tc.newSpecs) + updates, err := lke.ReconcileLKENodePoolSpecs(context.Background(), tc.oldSpecs, tc.newSpecs) if err != nil { t.Fatal(err) } diff --git a/linode/lke/framework_datasource_test.go b/linode/lke/framework_datasource_test.go index f3618db15..962ffbaa9 100644 --- a/linode/lke/framework_datasource_test.go +++ b/linode/lke/framework_datasource_test.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/linode/terraform-provider-linode/v2/linode/acceptance" "github.com/linode/terraform-provider-linode/v2/linode/lke/tmpl" - nodepooltmpl "github.com/linode/terraform-provider-linode/v2/linode/lkenodepool/tmpl" ) const dataSourceClusterName = "data.linode_lke_cluster.test" @@ -19,7 +18,6 @@ func TestAccDataSourceLKECluster_taints_labels(t *testing.T) { acceptance.RunTestRetry(t, 2, func(tRetry *acceptance.TRetry) { clusterName := acctest.RandomWithPrefix("tf_test") - poolTag := acctest.RandomWithPrefix("tf_test_") resource.Test(tRetry, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, @@ -27,33 +25,19 @@ func TestAccDataSourceLKECluster_taints_labels(t *testing.T) { Steps: []resource.TestStep{ { Config: tmpl.DataTaintsLabels( - t, &nodepooltmpl.TemplateData{ - K8sVersion: k8sVersionLatest, - Region: testRegion, - PoolNodeType: "g6-standard-1", - NodeCount: 1, - ClusterLabel: clusterName, - Taints: []nodepooltmpl.TaintData{ - { - Effect: "PreferNoSchedule", - Key: "foo", - Value: "bar", - }, + t, clusterName, k8sVersionLatest, testRegion, []tmpl.TaintData{ + { + Effect: "PreferNoSchedule", + Key: "foo", + Value: "bar", }, - PoolTag: poolTag, - Labels: map[string]string{"foo": "bar"}, }, + map[string]string{"foo": "bar"}, ), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(dataSourceClusterName, "pools.1.nodes.#", "1"), - acceptance.AnyOfTestCheckFunc( - resource.TestCheckResourceAttr(dataSourceClusterName, "pools.0.taints.#", "1"), - resource.TestCheckResourceAttr(dataSourceClusterName, "pools.1.taints.#", "1"), - ), - acceptance.AnyOfTestCheckFunc( - resource.TestCheckResourceAttr(dataSourceClusterName, "pools.0.labels.foo", "bar"), - resource.TestCheckResourceAttr(dataSourceClusterName, "pools.1.labels.foo", "bar"), - ), + resource.TestCheckResourceAttr(dataSourceClusterName, "pools.0.nodes.#", "1"), + resource.TestCheckResourceAttr(dataSourceClusterName, "pools.0.taints.#", "1"), + resource.TestCheckResourceAttr(dataSourceClusterName, "pools.0.labels.foo", "bar"), ), }, }, diff --git a/linode/lke/resource_test.go b/linode/lke/framework_resource_test.go similarity index 85% rename from linode/lke/resource_test.go rename to linode/lke/framework_resource_test.go index 5c75a6cd5..72218b1b5 100644 --- a/linode/lke/resource_test.go +++ b/linode/lke/framework_resource_test.go @@ -164,6 +164,19 @@ func waitForAllNodesReady(t *testing.T, cluster *linodego.LKECluster, pollInterv } } +func TestSmokeTests_lke(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceLKECluster_basic_smoke", TestAccResourceLKECluster_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceLKECluster_basic_smoke(t *testing.T) { t.Parallel() @@ -589,3 +602,96 @@ func TestAccResourceLKECluster_implicitCount(t *testing.T) { }, }) } + +func TestAccResourceLKEClusterNodePoolTaintsLabels(t *testing.T) { + t.Parallel() + + acceptance.RunTestRetry(t, 2, func(tRetry *acceptance.TRetry) { + clusterName := acctest.RandomWithPrefix("tf_test") + resource.Test(tRetry, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: acceptance.CheckLKEClusterDestroy, + Steps: []resource.TestStep{ + { + // create a cluster with taints and labels + Config: tmpl.DataTaintsLabels( + t, clusterName, k8sVersionLatest, testRegion, []tmpl.TaintData{ + { + Effect: "PreferNoSchedule", + Key: "foo", + Value: "bar", + }, + }, + map[string]string{"foo": "bar"}, + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.nodes.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.effect", "PreferNoSchedule"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.key", "foo"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.value", "bar"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.labels.foo", "bar"), + ), + }, + { + // update taints and labels values + Config: tmpl.DataTaintsLabels( + t, clusterName, k8sVersionLatest, testRegion, []tmpl.TaintData{ + { + Effect: "PreferNoSchedule", + Key: "baz", + Value: "qux", + }, + }, + map[string]string{"baz": "qux"}, + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.nodes.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.effect", "PreferNoSchedule"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.key", "baz"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.value", "qux"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.labels.baz", "qux"), + ), + }, + { + // remove taints and labels + Config: tmpl.DataTaintsLabels(t, clusterName, k8sVersionLatest, testRegion, nil, nil), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.nodes.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.#", "0"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.labels.#", "0"), + ), + }, + { + // add taints and labels back + Config: tmpl.DataTaintsLabels( + t, clusterName, k8sVersionLatest, testRegion, []tmpl.TaintData{ + { + Effect: "PreferNoSchedule", + Key: "foo", + Value: "bar", + }, + }, + map[string]string{"foo": "bar"}, + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.nodes.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.#", "1"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.effect", "PreferNoSchedule"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.key", "foo"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.taint.0.value", "bar"), + resource.TestCheckResourceAttr(resourceClusterName, "pool.0.labels.foo", "bar"), + ), + }, + { + ResourceName: resourceClusterName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"pool.0.nodes.0.status"}, + }, + }, + }) + }) +} diff --git a/linode/lke/resource.go b/linode/lke/resource.go index 0b7111602..3c3e4372e 100644 --- a/linode/lke/resource.go +++ b/linode/lke/resource.go @@ -123,7 +123,7 @@ func readResource(ctx context.Context, d *schema.ResourceData, meta interface{}) d.Set("dashboard_url", dashboard.URL) d.Set("api_endpoints", flattenLKEClusterAPIEndpoints(endpoints)) - matchedPools, err := matchPoolsWithSchema(pools, declaredPools) + matchedPools, err := matchPoolsWithSchema(ctx, pools, declaredPools) if err != nil { return diag.Errorf("failed to match api pools with schema: %s", err) } @@ -178,6 +178,8 @@ func createResource(ctx context.Context, d *schema.ResourceData, meta interface{ createOpts.NodePools = append(createOpts.NodePools, linodego.LKENodePoolCreateOptions{ Type: poolSpec["type"].(string), Tags: helper.ExpandStringSet(poolSpec["tags"].(*schema.Set)), + Taints: expandNodePoolTaints(helper.ExpandObjectSet(poolSpec["taint"].(*schema.Set))), + Labels: helper.StringAnyMapToTyped[string](poolSpec["labels"].(map[string]any)), Count: count, Autoscaler: autoscaler, }) @@ -281,6 +283,7 @@ func updateResource(ctx context.Context, d *schema.ResourceData, meta interface{ oldPools, newPools := d.GetChange("pool") updates, err := ReconcileLKENodePoolSpecs( + ctx, expandLinodeLKENodePoolSpecs(oldPools.([]any), false), expandLinodeLKENodePoolSpecs(newPools.([]any), true), ) diff --git a/linode/lke/schema_resource.go b/linode/lke/schema_resource.go index 9a6641bd9..ae297e961 100644 --- a/linode/lke/schema_resource.go +++ b/linode/lke/schema_resource.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/linode/linodego" "github.com/linode/terraform-provider-linode/v2/linode/helper" ) @@ -83,6 +84,53 @@ var resourceSchema = map[string]*schema.Schema{ Description: "A Linode Type for all of the nodes in the Node Pool.", Required: true, }, + "labels": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Key-value pairs added as labels to nodes in the node pool. " + + "Labels help classify your nodes and to easily select subsets of objects.", + Optional: true, + }, + "taint": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "effect": { + Type: schema.TypeString, + Description: "The Kubernetes taint effect.", + Required: true, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringInSlice([]string{ + string(linodego.LKENodePoolTaintEffectNoExecute), + string(linodego.LKENodePoolTaintEffectNoSchedule), + string(linodego.LKENodePoolTaintEffectPreferNoSchedule), + }, false), + ), + }, + "key": { + Description: "The Kubernetes taint key.", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringIsNotEmpty, + ), + }, + "value": { + Description: "The Kubernetes taint value.", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringIsNotEmpty, + ), + }, + }, + }, + Description: "Kubernetes taints to add to node pool nodes. Taints help control how " + + "pods are scheduled onto nodes, specifically allowing them to repel certain pods.", + Optional: true, + }, "tags": { Type: schema.TypeSet, Description: "A set of tags applied to this node pool.", diff --git a/linode/lke/tmpl/data_taints_labels.gotf b/linode/lke/tmpl/data_taints_labels.gotf index 1f27fa31f..297b040a6 100644 --- a/linode/lke/tmpl/data_taints_labels.gotf +++ b/linode/lke/tmpl/data_taints_labels.gotf @@ -1,10 +1,9 @@ {{ define "lke_cluster_data_taints_labels" }} -{{ template "nodepool_template" . }} +{{ template "lke_cluster_taints_labels" . }} data "linode_lke_cluster" "test" { - depends_on = [ linode_lke_node_pool.foobar ] - id = linode_lke_cluster.nodepool_test_cluster.id + id = linode_lke_cluster.test.id } {{ end }} diff --git a/linode/lke/tmpl/taints_labels.gotf b/linode/lke/tmpl/taints_labels.gotf new file mode 100644 index 000000000..43b522522 --- /dev/null +++ b/linode/lke/tmpl/taints_labels.gotf @@ -0,0 +1,31 @@ +{{ define "lke_cluster_taints_labels" }} + +resource "linode_lke_cluster" "test" { + label = "{{ .Label }}" + region = "{{ .Region }}" + k8s_version = "{{ .K8sVersion }}" + tags = ["test"] + + pool { + type = "g6-standard-1" + count = 1 + tags = ["test"] + +{{ range $taint := .Taints }} + taint { + effect = "{{ $taint.Effect }}" + key = "{{ $taint.Key }}" + value = "{{ $taint.Value }}" + } +{{ end }} + +{{ range $key, $val := .Labels }} + labels = { + "{{ $key }}" = "{{ $val }}" + } +{{ end }} + } +} + +{{ end }} + diff --git a/linode/lke/tmpl/template.go b/linode/lke/tmpl/template.go index 295b7b4ad..ee508f6f3 100644 --- a/linode/lke/tmpl/template.go +++ b/linode/lke/tmpl/template.go @@ -4,9 +4,14 @@ import ( "testing" "github.com/linode/terraform-provider-linode/v2/linode/acceptance" - nodepooltmpl "github.com/linode/terraform-provider-linode/v2/linode/lkenodepool/tmpl" ) +type TaintData struct { + Effect string + Key string + Value string +} + type TemplateData struct { Label string K8sVersion string @@ -15,6 +20,8 @@ type TemplateData struct { ACLEnabled bool IPv4 string IPv6 string + Taints []TaintData + Labels map[string]string } func Basic(t *testing.T, name, version, region string) string { @@ -110,6 +117,13 @@ func DataControlPlane(t *testing.T, name, version, region, ipv4, ipv6 string, ha }) } -func DataTaintsLabels(t *testing.T, data *nodepooltmpl.TemplateData) string { - return acceptance.ExecuteTemplate(t, "lke_cluster_data_taints_labels", *data) +func DataTaintsLabels(t *testing.T, name, version, region string, taints []TaintData, labels map[string]string) string { + return acceptance.ExecuteTemplate(t, + "lke_cluster_data_taints_labels", TemplateData{ + Label: name, + K8sVersion: version, + Region: region, + Labels: labels, + Taints: taints, + }) } diff --git a/linode/lketypes/datasource_test.go b/linode/lketypes/datasource_test.go new file mode 100644 index 000000000..a7dc0a276 --- /dev/null +++ b/linode/lketypes/datasource_test.go @@ -0,0 +1,35 @@ +//go:build integration || lketypes + +package lketypes_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" + "github.com/linode/terraform-provider-linode/v2/linode/lketypes/tmpl" +) + +func TestAccDataSourceLKETypes_basic(t *testing.T) { + t.Parallel() + + dataSourceName := "data.linode_lke_types.foobar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: tmpl.DataBasic(t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "types.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "types.0.id", "lke-sa"), + resource.TestCheckResourceAttr(dataSourceName, "types.0.label", "LKE Standard Availability"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.transfer"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.price.0.hourly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.price.0.monthly"), + ), + }, + }, + }) +} diff --git a/linode/lketypes/framework_datasource.go b/linode/lketypes/framework_datasource.go new file mode 100644 index 000000000..f35b16a77 --- /dev/null +++ b/linode/lketypes/framework_datasource.go @@ -0,0 +1,73 @@ +package lketypes + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v2/linode/helper" +) + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_lke_types", + Schema: &frameworkDataSourceSchema, + }, + ), + } +} + +type DataSource struct { + helper.BaseDataSource +} + +func (r *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data.linode_lke_types") + + var data LKETypeFilterModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id, d := filterConfig.GenerateID(data.Filters) + if d != nil { + resp.Diagnostics.Append(d) + return + } + data.ID = id + + result, d := filterConfig.GetAndFilter( + ctx, r.Meta.Client, data.Filters, listLKETypes, data.Order, data.OrderBy) + if d != nil { + resp.Diagnostics.Append(d) + return + } + + data.parseLKETypes(helper.AnySliceToTyped[linodego.LKEType](result)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func listLKETypes(ctx context.Context, client *linodego.Client, filter string) ([]any, error) { + tflog.Debug(ctx, "Listing LKE types", map[string]any{ + "filter_header": filter, + }) + + types, err := client.ListLKETypes(ctx, &linodego.ListOptions{ + Filter: filter, + }) + if err != nil { + return nil, err + } + + return helper.TypedSliceToAny(types), nil +} diff --git a/linode/lketypes/framework_datasource_schema.go b/linode/lketypes/framework_datasource_schema.go new file mode 100644 index 000000000..7c3af3117 --- /dev/null +++ b/linode/lketypes/framework_datasource_schema.go @@ -0,0 +1,34 @@ +package lketypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/linode/terraform-provider-linode/v2/linode/helper/frameworkfilter" +) + +var lkeTypeSchema = schema.NestedBlockObject{ + Attributes: helper.GetPricingTypeAttributes("LKE Type"), +} + +var filterConfig = frameworkfilter.Config{ + "label": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeString}, + "transfer": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeInt}, +} + +var frameworkDataSourceSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The data source's unique ID.", + Computed: true, + }, + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), + }, + Blocks: map[string]schema.Block{ + "filter": filterConfig.Schema(), + "types": schema.ListNestedBlock{ + Description: "The returned list of LKE types.", + NestedObject: lkeTypeSchema, + }, + }, +} diff --git a/linode/lketypes/framework_models.go b/linode/lketypes/framework_models.go new file mode 100644 index 000000000..b357dfbfd --- /dev/null +++ b/linode/lketypes/framework_models.go @@ -0,0 +1,121 @@ +package lketypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/linode/terraform-provider-linode/v2/linode/helper/frameworkfilter" +) + +type DataSourceModel struct { + ID types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` + Price types.List `tfsdk:"price"` + RegionPrices types.List `tfsdk:"region_prices"` + Transfer types.Int64 `tfsdk:"transfer"` +} + +func (data *DataSourceModel) parseLKEType(lkeType *linodego.LKEType, +) diag.Diagnostics { + data.ID = types.StringValue(lkeType.ID) + + price, diags := flattenPrice(lkeType.Price) + if diags.HasError() { + return diags + } + data.Price = *price + + data.Label = types.StringValue(lkeType.Label) + + regionPrices, d := flattenRegionPrices(lkeType.RegionPrices) + if d.HasError() { + return d + } + data.RegionPrices = *regionPrices + + data.Transfer = types.Int64Value(int64(lkeType.Transfer)) + + return nil +} + +func flattenPrice(price linodego.LKETypePrice) ( + *basetypes.ListValue, diag.Diagnostics, +) { + result := make(map[string]attr.Value) + + result["hourly"] = types.Float64Value(float64(price.Hourly)) + result["monthly"] = types.Float64Value(float64(price.Monthly)) + + obj, diag := types.ObjectValue(helper.PriceObjectType.AttrTypes, result) + if diag.HasError() { + return nil, diag + } + + objList := []attr.Value{obj} + + resultList, diag := types.ListValue( + helper.PriceObjectType, + objList, + ) + if diag.HasError() { + return nil, diag + } + + return &resultList, nil +} + +func flattenRegionPrices(prices []linodego.LKETypeRegionPrice) ( + *basetypes.ListValue, diag.Diagnostics, +) { + result := make([]attr.Value, len(prices)) + + for i, price := range prices { + obj, d := types.ObjectValue(helper.RegionPriceObjectType.AttrTypes, map[string]attr.Value{ + "id": types.StringValue(price.ID), + "hourly": types.Float64Value(float64(price.Hourly)), + "monthly": types.Float64Value(float64(price.Monthly)), + }) + if d.HasError() { + return nil, d + } + + result[i] = obj + } + + priceList, d := basetypes.NewListValue( + helper.RegionPriceObjectType, + result, + ) + return &priceList, d +} + +type LKETypeFilterModel struct { + ID types.String `tfsdk:"id"` + Order types.String `tfsdk:"order"` + OrderBy types.String `tfsdk:"order_by"` + Filters frameworkfilter.FiltersModelType `tfsdk:"filter"` + Types []DataSourceModel `tfsdk:"types"` +} + +func (model *LKETypeFilterModel) parseLKETypes(lkeTypes []linodego.LKEType, +) diag.Diagnostics { + result := make([]DataSourceModel, len(lkeTypes)) + + for i := range lkeTypes { + var m DataSourceModel + + diags := m.parseLKEType(&lkeTypes[i]) + if diags.HasError() { + return diags + } + + result[i] = m + } + + model.Types = result + + return nil +} diff --git a/linode/lketypes/tmpl/data_basic.gotf b/linode/lketypes/tmpl/data_basic.gotf new file mode 100644 index 000000000..5b260627c --- /dev/null +++ b/linode/lketypes/tmpl/data_basic.gotf @@ -0,0 +1,10 @@ +{{ define "lke_types_data_basic" }} + +data "linode_lke_types" "foobar" { + filter { + name = "label" + values = ["LKE Standard Availability"] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/lketypes/tmpl/template.go b/linode/lketypes/tmpl/template.go new file mode 100644 index 000000000..687ebbe6b --- /dev/null +++ b/linode/lketypes/tmpl/template.go @@ -0,0 +1,12 @@ +package tmpl + +import ( + "testing" + + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" +) + +func DataBasic(t *testing.T) string { + return acceptance.ExecuteTemplate(t, + "lke_types_data_basic", nil) +} diff --git a/linode/nb/resource_test.go b/linode/nb/resource_test.go index ed8443c2f..1c2554e71 100644 --- a/linode/nb/resource_test.go +++ b/linode/nb/resource_test.go @@ -62,6 +62,19 @@ func sweep(prefix string) error { return nil } +func TestSmokeTests_nb(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceNodeBalancer_basic_smoke", TestAccResourceNodeBalancer_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceNodeBalancer_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/nbtypes/datasource_test.go b/linode/nbtypes/datasource_test.go new file mode 100644 index 000000000..d97eb41dc --- /dev/null +++ b/linode/nbtypes/datasource_test.go @@ -0,0 +1,38 @@ +//go:build integration || nbtypes + +package nbtypes_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" + "github.com/linode/terraform-provider-linode/v2/linode/nbtypes/tmpl" +) + +func TestAccDataSourceNodeBalancerTypes_basic(t *testing.T) { + t.Parallel() + + dataSourceName := "data.linode_nb_types.foobar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: tmpl.DataBasic(t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "types.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "types.0.id", "nodebalancer"), + resource.TestCheckResourceAttr(dataSourceName, "types.0.label", "NodeBalancer"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.transfer"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.price.0.hourly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.price.0.monthly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.id"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.hourly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.monthly"), + ), + }, + }, + }) +} diff --git a/linode/nbtypes/framework_datasource.go b/linode/nbtypes/framework_datasource.go new file mode 100644 index 000000000..bcaf07679 --- /dev/null +++ b/linode/nbtypes/framework_datasource.go @@ -0,0 +1,73 @@ +package nbtypes + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v2/linode/helper" +) + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_nb_types", + Schema: &frameworkDataSourceSchema, + }, + ), + } +} + +type DataSource struct { + helper.BaseDataSource +} + +func (r *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data.linode_nb_types") + + var data NodeBalancerTypeFilterModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id, d := filterConfig.GenerateID(data.Filters) + if d != nil { + resp.Diagnostics.Append(d) + return + } + data.ID = id + + result, d := filterConfig.GetAndFilter( + ctx, r.Meta.Client, data.Filters, listNodeBalancerTypes, data.Order, data.OrderBy) + if d != nil { + resp.Diagnostics.Append(d) + return + } + + data.parseNodeBalancerTypes(helper.AnySliceToTyped[linodego.NodeBalancerType](result)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func listNodeBalancerTypes(ctx context.Context, client *linodego.Client, filter string) ([]any, error) { + tflog.Debug(ctx, "Listing Node Balancer types", map[string]any{ + "filter_header": filter, + }) + + types, err := client.ListNodeBalancerTypes(ctx, &linodego.ListOptions{ + Filter: filter, + }) + if err != nil { + return nil, err + } + + return helper.TypedSliceToAny(types), nil +} diff --git a/linode/nbtypes/framework_datasource_schema.go b/linode/nbtypes/framework_datasource_schema.go new file mode 100644 index 000000000..89bc9c327 --- /dev/null +++ b/linode/nbtypes/framework_datasource_schema.go @@ -0,0 +1,34 @@ +package nbtypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/linode/terraform-provider-linode/v2/linode/helper/frameworkfilter" +) + +var nodebalancerTypeSchema = schema.NestedBlockObject{ + Attributes: helper.GetPricingTypeAttributes("Node Balancer Type"), +} + +var filterConfig = frameworkfilter.Config{ + "label": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeString}, + "transfer": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeInt}, +} + +var frameworkDataSourceSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The data source's unique ID.", + Computed: true, + }, + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), + }, + Blocks: map[string]schema.Block{ + "filter": filterConfig.Schema(), + "types": schema.ListNestedBlock{ + Description: "The returned list of Node Balancer types.", + NestedObject: nodebalancerTypeSchema, + }, + }, +} diff --git a/linode/nbtypes/framework_models.go b/linode/nbtypes/framework_models.go new file mode 100644 index 000000000..6b0010213 --- /dev/null +++ b/linode/nbtypes/framework_models.go @@ -0,0 +1,121 @@ +package nbtypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/linode/terraform-provider-linode/v2/linode/helper/frameworkfilter" +) + +type DataSourceModel struct { + ID types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` + Price types.List `tfsdk:"price"` + RegionPrices types.List `tfsdk:"region_prices"` + Transfer types.Int64 `tfsdk:"transfer"` +} + +func (data *DataSourceModel) parseNodeBalancerType(nbType *linodego.NodeBalancerType, +) diag.Diagnostics { + data.ID = types.StringValue(nbType.ID) + + price, diags := flattenPrice(nbType.Price) + if diags.HasError() { + return diags + } + data.Price = *price + + data.Label = types.StringValue(nbType.Label) + + regionPrices, d := flattenRegionPrices(nbType.RegionPrices) + if d.HasError() { + return d + } + data.RegionPrices = *regionPrices + + data.Transfer = types.Int64Value(int64(nbType.Transfer)) + + return nil +} + +func flattenPrice(price linodego.NodeBalancerTypePrice) ( + *basetypes.ListValue, diag.Diagnostics, +) { + result := make(map[string]attr.Value) + + result["hourly"] = types.Float64Value(float64(price.Hourly)) + result["monthly"] = types.Float64Value(float64(price.Monthly)) + + obj, diag := types.ObjectValue(helper.PriceObjectType.AttrTypes, result) + if diag.HasError() { + return nil, diag + } + + objList := []attr.Value{obj} + + resultList, diag := types.ListValue( + helper.PriceObjectType, + objList, + ) + if diag.HasError() { + return nil, diag + } + + return &resultList, nil +} + +func flattenRegionPrices(prices []linodego.NodeBalancerTypeRegionPrice) ( + *basetypes.ListValue, diag.Diagnostics, +) { + result := make([]attr.Value, len(prices)) + + for i, price := range prices { + obj, d := types.ObjectValue(helper.RegionPriceObjectType.AttrTypes, map[string]attr.Value{ + "id": types.StringValue(price.ID), + "hourly": types.Float64Value(float64(price.Hourly)), + "monthly": types.Float64Value(float64(price.Monthly)), + }) + if d.HasError() { + return nil, d + } + + result[i] = obj + } + + priceList, d := basetypes.NewListValue( + helper.RegionPriceObjectType, + result, + ) + return &priceList, d +} + +type NodeBalancerTypeFilterModel struct { + ID types.String `tfsdk:"id"` + Order types.String `tfsdk:"order"` + OrderBy types.String `tfsdk:"order_by"` + Filters frameworkfilter.FiltersModelType `tfsdk:"filter"` + Types []DataSourceModel `tfsdk:"types"` +} + +func (model *NodeBalancerTypeFilterModel) parseNodeBalancerTypes(nbTypes []linodego.NodeBalancerType, +) diag.Diagnostics { + result := make([]DataSourceModel, len(nbTypes)) + + for i := range nbTypes { + var m DataSourceModel + + diags := m.parseNodeBalancerType(&nbTypes[i]) + if diags.HasError() { + return diags + } + + result[i] = m + } + + model.Types = result + + return nil +} diff --git a/linode/nbtypes/tmpl/data_basic.gotf b/linode/nbtypes/tmpl/data_basic.gotf new file mode 100644 index 000000000..fb8fffe12 --- /dev/null +++ b/linode/nbtypes/tmpl/data_basic.gotf @@ -0,0 +1,10 @@ +{{ define "nb_types_data_basic" }} + +data "linode_nb_types" "foobar" { + filter { + name = "label" + values = ["NodeBalancer"] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/nbtypes/tmpl/template.go b/linode/nbtypes/tmpl/template.go new file mode 100644 index 000000000..b57ade428 --- /dev/null +++ b/linode/nbtypes/tmpl/template.go @@ -0,0 +1,12 @@ +package tmpl + +import ( + "testing" + + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" +) + +func DataBasic(t *testing.T) string { + return acceptance.ExecuteTemplate(t, + "nb_types_data_basic", nil) +} diff --git a/linode/networktransferprices/datasource_test.go b/linode/networktransferprices/datasource_test.go new file mode 100644 index 000000000..018966654 --- /dev/null +++ b/linode/networktransferprices/datasource_test.go @@ -0,0 +1,38 @@ +//go:build integration || networktransferprices + +package networktransferprices_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" + "github.com/linode/terraform-provider-linode/v2/linode/networktransferprices/tmpl" +) + +func TestAccDataSourceNetworkTransferPrices_basic(t *testing.T) { + t.Parallel() + + dataSourceName := "data.linode_network_transfer_prices.foobar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: tmpl.DataBasic(t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "types.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "types.0.id", "network_transfer"), + resource.TestCheckResourceAttr(dataSourceName, "types.0.label", "Network Transfer"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.transfer"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.price.0.hourly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.price.0.monthly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.id"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.hourly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.monthly"), + ), + }, + }, + }) +} diff --git a/linode/networktransferprices/framework_datasource.go b/linode/networktransferprices/framework_datasource.go new file mode 100644 index 000000000..7700e1fcb --- /dev/null +++ b/linode/networktransferprices/framework_datasource.go @@ -0,0 +1,73 @@ +package networktransferprices + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v2/linode/helper" +) + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_network_transfer_prices", + Schema: &frameworkDataSourceSchema, + }, + ), + } +} + +type DataSource struct { + helper.BaseDataSource +} + +func (r *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data.linode_network_transfer_prices") + + var data NetworkTransferPriceFilterModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id, d := filterConfig.GenerateID(data.Filters) + if d != nil { + resp.Diagnostics.Append(d) + return + } + data.ID = id + + result, d := filterConfig.GetAndFilter( + ctx, r.Meta.Client, data.Filters, listNetworkTransferPrices, data.Order, data.OrderBy) + if d != nil { + resp.Diagnostics.Append(d) + return + } + + data.parseNetworkTransferPrices(helper.AnySliceToTyped[linodego.NetworkTransferPrice](result)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func listNetworkTransferPrices(ctx context.Context, client *linodego.Client, filter string) ([]any, error) { + tflog.Debug(ctx, "Listing Network Transfer Prices", map[string]any{ + "filter_header": filter, + }) + + types, err := client.ListNetworkTransferPrices(ctx, &linodego.ListOptions{ + Filter: filter, + }) + if err != nil { + return nil, err + } + + return helper.TypedSliceToAny(types), nil +} diff --git a/linode/networktransferprices/framework_datasource_schema.go b/linode/networktransferprices/framework_datasource_schema.go new file mode 100644 index 000000000..3db54b351 --- /dev/null +++ b/linode/networktransferprices/framework_datasource_schema.go @@ -0,0 +1,34 @@ +package networktransferprices + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/linode/terraform-provider-linode/v2/linode/helper/frameworkfilter" +) + +var networkTransferPriceSchema = schema.NestedBlockObject{ + Attributes: helper.GetPricingTypeAttributes("Network Transfer Price"), +} + +var filterConfig = frameworkfilter.Config{ + "label": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeString}, + "transfer": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeInt}, +} + +var frameworkDataSourceSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The data source's unique ID.", + Computed: true, + }, + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), + }, + Blocks: map[string]schema.Block{ + "filter": filterConfig.Schema(), + "types": schema.ListNestedBlock{ + Description: "The returned list of Network Transfer Prices.", + NestedObject: networkTransferPriceSchema, + }, + }, +} diff --git a/linode/networktransferprices/framework_models.go b/linode/networktransferprices/framework_models.go new file mode 100644 index 000000000..3efb1901a --- /dev/null +++ b/linode/networktransferprices/framework_models.go @@ -0,0 +1,121 @@ +package networktransferprices + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/linode/terraform-provider-linode/v2/linode/helper/frameworkfilter" +) + +type DataSourceModel struct { + ID types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` + Price types.List `tfsdk:"price"` + RegionPrices types.List `tfsdk:"region_prices"` + Transfer types.Int64 `tfsdk:"transfer"` +} + +func (data *DataSourceModel) parseNetworkTransferPrice(networkTransferPrice *linodego.NetworkTransferPrice, +) diag.Diagnostics { + data.ID = types.StringValue(networkTransferPrice.ID) + + price, diags := flattenPrice(networkTransferPrice.Price) + if diags.HasError() { + return diags + } + data.Price = *price + + data.Label = types.StringValue(networkTransferPrice.Label) + + regionPrices, d := flattenRegionPrices(networkTransferPrice.RegionPrices) + if d.HasError() { + return d + } + data.RegionPrices = *regionPrices + + data.Transfer = types.Int64Value(int64(networkTransferPrice.Transfer)) + + return nil +} + +func flattenPrice(price linodego.NetworkTransferTypePrice) ( + *basetypes.ListValue, diag.Diagnostics, +) { + result := make(map[string]attr.Value) + + result["hourly"] = types.Float64Value(float64(price.Hourly)) + result["monthly"] = types.Float64Value(float64(price.Monthly)) + + obj, diag := types.ObjectValue(helper.PriceObjectType.AttrTypes, result) + if diag.HasError() { + return nil, diag + } + + objList := []attr.Value{obj} + + resultList, diag := types.ListValue( + helper.PriceObjectType, + objList, + ) + if diag.HasError() { + return nil, diag + } + + return &resultList, nil +} + +func flattenRegionPrices(prices []linodego.NetworkTransferTypeRegionPrice) ( + *basetypes.ListValue, diag.Diagnostics, +) { + result := make([]attr.Value, len(prices)) + + for i, price := range prices { + obj, d := types.ObjectValue(helper.RegionPriceObjectType.AttrTypes, map[string]attr.Value{ + "id": types.StringValue(price.ID), + "hourly": types.Float64Value(float64(price.Hourly)), + "monthly": types.Float64Value(float64(price.Monthly)), + }) + if d.HasError() { + return nil, d + } + + result[i] = obj + } + + priceList, d := basetypes.NewListValue( + helper.RegionPriceObjectType, + result, + ) + return &priceList, d +} + +type NetworkTransferPriceFilterModel struct { + ID types.String `tfsdk:"id"` + Order types.String `tfsdk:"order"` + OrderBy types.String `tfsdk:"order_by"` + Filters frameworkfilter.FiltersModelType `tfsdk:"filter"` + Types []DataSourceModel `tfsdk:"types"` +} + +func (model *NetworkTransferPriceFilterModel) parseNetworkTransferPrices(networkTransferPrices []linodego.NetworkTransferPrice, +) diag.Diagnostics { + result := make([]DataSourceModel, len(networkTransferPrices)) + + for i := range networkTransferPrices { + var m DataSourceModel + + diags := m.parseNetworkTransferPrice(&networkTransferPrices[i]) + if diags.HasError() { + return diags + } + + result[i] = m + } + + model.Types = result + + return nil +} diff --git a/linode/networktransferprices/tmpl/data_basic.gotf b/linode/networktransferprices/tmpl/data_basic.gotf new file mode 100644 index 000000000..423075e6f --- /dev/null +++ b/linode/networktransferprices/tmpl/data_basic.gotf @@ -0,0 +1,10 @@ +{{ define "network_transfer_prices_data_basic" }} + +data "linode_network_transfer_prices" "foobar" { + filter { + name = "label" + values = ["Network Transfer"] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/networktransferprices/tmpl/template.go b/linode/networktransferprices/tmpl/template.go new file mode 100644 index 000000000..fe21d70d6 --- /dev/null +++ b/linode/networktransferprices/tmpl/template.go @@ -0,0 +1,12 @@ +package tmpl + +import ( + "testing" + + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" +) + +func DataBasic(t *testing.T) string { + return acceptance.ExecuteTemplate(t, + "network_transfer_prices_data_basic", nil) +} diff --git a/linode/objbucket/resource_test.go b/linode/objbucket/resource_test.go index 9277bf8f3..95c22d4f3 100644 --- a/linode/objbucket/resource_test.go +++ b/linode/objbucket/resource_test.go @@ -171,6 +171,20 @@ func sweep(prefix string) error { return nil } +func TestSmokeTests_objbucket(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceBucket_basic_legacy_smoke", TestAccResourceBucket_basic_legacy_smoke}, + {"TestAccResourceBucket_basic_smoke", TestAccResourceBucket_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceBucket_basic_legacy_smoke(t *testing.T) { t.Parallel() diff --git a/linode/provider.go b/linode/provider.go index dbd3ff674..8a9fcbf56 100644 --- a/linode/provider.go +++ b/linode/provider.go @@ -61,6 +61,11 @@ func Provider() *schema.Provider { Optional: true, Description: "The version of Linode API.", }, + "api_ca_path": { + Type: schema.TypeString, + Optional: true, + Description: "The path to a Linode API CA file to trust.", + }, "skip_instance_ready_poll": { Type: schema.TypeBool, @@ -182,6 +187,12 @@ func handleDefault(config *helper.Config, d *schema.ResourceData) diag.Diagnosti config.APIVersion = os.Getenv("LINODE_API_VERSION") } + if v, ok := d.GetOk("api_ca_path"); ok { + config.APICAPath = v.(string) + } else { + config.APICAPath = os.Getenv(linodego.APIHostCert) + } + if v, ok := d.GetOk("config_path"); ok { config.ConfigPath = v.(string) } else { diff --git a/linode/provider_test.go b/linode/provider_test.go index 3adee1b52..f4d2ca739 100644 --- a/linode/provider_test.go +++ b/linode/provider_test.go @@ -1,4 +1,4 @@ -//go:build integration || vpcsubnets +//go:build integration || provider package linode_test @@ -20,6 +20,7 @@ func TestAccProvider_Overrides(t *testing.T) { token = 54321 api_url = https://cool.linode.com api_version = v4reallycoolapiversion +api_cert_path = cert.pem [cool] token = %s diff --git a/linode/regions/datasource_test.go b/linode/regions/datasource_test.go index 36faa22a3..789809879 100644 --- a/linode/regions/datasource_test.go +++ b/linode/regions/datasource_test.go @@ -13,6 +13,19 @@ import ( "github.com/linode/terraform-provider-linode/v2/linode/regions/tmpl" ) +func TestSmokeTests_firewall(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccDataSourceRegions_basic_smoke", TestAccDataSourceRegions_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccDataSourceRegions_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/stackscript/resource_test.go b/linode/stackscript/resource_test.go index b22e4a3ea..09e077741 100644 --- a/linode/stackscript/resource_test.go +++ b/linode/stackscript/resource_test.go @@ -48,6 +48,19 @@ func sweep(prefix string) error { return nil } +func TestSmokeTests_stackscript(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceStackscript_basic_smoke", TestAccResourceStackscript_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceStackscript_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/stackscripts/datasource_test.go b/linode/stackscripts/datasource_test.go index f5e894787..ba10ca9a4 100644 --- a/linode/stackscripts/datasource_test.go +++ b/linode/stackscripts/datasource_test.go @@ -17,6 +17,19 @@ var basicStackScript = `#!/bin/bash echo "Hello, $NAME!" ` +func TestSmokeTests_stackscripts(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccDataSourceStackscripts_basic_smoke", TestAccDataSourceStackscripts_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccDataSourceStackscripts_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/volume/datasource_test.go b/linode/volume/datasource_test.go index f091da567..944a0d37f 100644 --- a/linode/volume/datasource_test.go +++ b/linode/volume/datasource_test.go @@ -31,6 +31,37 @@ func TestAccDataSourceVolume_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "linode_id", "0"), resource.TestCheckResourceAttrSet(resourceName, "created"), resource.TestCheckResourceAttrSet(resourceName, "updated"), + resource.TestCheckResourceAttr(resourceName, "encryption", "disabled"), + ), + }, + }, + }) +} + +func TestAccDataSourceVolume_withBlockStorageEncryption(t *testing.T) { + t.Parallel() + + volumeName := acctest.RandomWithPrefix("tf_test") + resourceName := "data.linode_volume.foobar" + + // Resolve a region with support for Block Storage Encryption + targetRegion, err := acceptance.GetRandomRegionWithCaps( + []string{"Linodes", "Block Storage Encryption"}, + "core", + ) + if err != nil { + t.Fatal(err) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: tmpl.DataWithBlockStorageEncryption(t, volumeName, targetRegion), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "region", targetRegion), + resource.TestCheckResourceAttr(resourceName, "encryption", "enabled"), ), }, }, diff --git a/linode/volume/framework_models.go b/linode/volume/framework_models.go index 832b71366..5cc24ed60 100644 --- a/linode/volume/framework_models.go +++ b/linode/volume/framework_models.go @@ -24,6 +24,7 @@ type VolumeDataSourceModel struct { Status types.String `tfsdk:"status"` Created types.String `tfsdk:"created"` Updated types.String `tfsdk:"updated"` + Encryption types.String `tfsdk:"encryption"` } func (data *VolumeDataSourceModel) ParseComputedAttributes( @@ -44,6 +45,7 @@ func (data *VolumeDataSourceModel) ParseComputedAttributes( data.FilesystemPath = types.StringValue(volume.FilesystemPath) data.Created = types.StringValue(volume.Created.Format(time.RFC3339)) data.Updated = types.StringValue(volume.Updated.Format(time.RFC3339)) + data.Encryption = types.StringValue(volume.Encryption) return diags } @@ -76,6 +78,7 @@ type VolumeResourceModel struct { Tags types.Set `tfsdk:"tags"` Status types.String `tfsdk:"status"` Timeouts timeouts.Value `tfsdk:"timeouts"` + Encryption types.String `tfsdk:"encryption"` } func (data *VolumeResourceModel) FlattenVolume(volume *linodego.Volume, preserveKnown bool) diag.Diagnostics { @@ -91,6 +94,7 @@ func (data *VolumeResourceModel) FlattenVolume(volume *linodego.Volume, preserve data.Label = helper.KeepOrUpdateString(data.Label, volume.Label, preserveKnown) data.Region = helper.KeepOrUpdateString(data.Region, volume.Region, preserveKnown) data.Size = helper.KeepOrUpdateInt64(data.Size, int64(volume.Size), preserveKnown) + data.Encryption = helper.KeepOrUpdateString(data.Encryption, volume.Encryption, preserveKnown) // planned breaking change: // data.LinodeID = helper.KeepOrUpdateIntPointer(data.LinodeID, volume.LinodeID, preserveKnown) @@ -123,6 +127,7 @@ func (data *VolumeResourceModel) CopyFrom(other VolumeResourceModel, preserveKno data.ID = helper.KeepOrUpdateValue(data.ID, other.ID, preserveKnown) data.Label = helper.KeepOrUpdateValue(data.Label, other.Label, preserveKnown) data.Region = helper.KeepOrUpdateValue(data.Region, other.Region, preserveKnown) + data.Encryption = helper.KeepOrUpdateValue(data.Encryption, other.Encryption, preserveKnown) data.Size = helper.KeepOrUpdateValue(data.Size, other.Size, preserveKnown) data.LinodeID = helper.KeepOrUpdateValue(data.LinodeID, other.LinodeID, preserveKnown) data.FilesystemPath = helper.KeepOrUpdateValue(data.FilesystemPath, other.FilesystemPath, preserveKnown) diff --git a/linode/volume/framework_models_unit_test.go b/linode/volume/framework_models_unit_test.go index 67366a2ce..cba0c3aba 100644 --- a/linode/volume/framework_models_unit_test.go +++ b/linode/volume/framework_models_unit_test.go @@ -29,6 +29,7 @@ func TestVolumeModelParsing(t *testing.T) { Tags: []string{"example tag", "another example"}, Created: &createdTime, Updated: &updatedTime, + Encryption: "disabled", } ctx := context.Background() @@ -43,6 +44,7 @@ func TestVolumeModelParsing(t *testing.T) { assert.Equal(t, types.Int64Value(30), data.Size) assert.Equal(t, types.Int64Value(12346), data.LinodeID) assert.Equal(t, types.StringValue("/dev/disk/by-id/scsi-0Linode_Volume_my-volume"), data.FilesystemPath) + assert.Equal(t, types.StringValue("disabled"), data.Encryption) assert.NotContains(t, data.Tags.String(), "example tag") assert.NotContains(t, data.Tags.String(), "another example") diff --git a/linode/volume/framework_resource.go b/linode/volume/framework_resource.go index 23bf0bad6..d05625ff3 100644 --- a/linode/volume/framework_resource.go +++ b/linode/volume/framework_resource.go @@ -216,9 +216,10 @@ func (r *Resource) CreateVolume( size := helper.FrameworkSafeInt64ToInt(data.Size.ValueInt64(), diags) createOpts := linodego.VolumeCreateOptions{ - Label: data.Label.ValueString(), - Region: data.Region.ValueString(), - Size: size, + Label: data.Label.ValueString(), + Region: data.Region.ValueString(), + Size: size, + Encryption: data.Encryption.ValueString(), } diags.Append(data.Tags.ElementsAs(ctx, &createOpts.Tags, false)...) diff --git a/linode/volume/framework_schema_datasource.go b/linode/volume/framework_schema_datasource.go index f1f952b69..93ce1c7bb 100644 --- a/linode/volume/framework_schema_datasource.go +++ b/linode/volume/framework_schema_datasource.go @@ -48,6 +48,11 @@ var VolumeAttributes = map[string]schema.Attribute{ Description: "Datetime string representing when the Volume was last updated.", Computed: true, }, + "encryption": schema.StringAttribute{ + Description: "Whether Block Storage Disk Encryption is enabled or disabled on this Volume. " + + "Note: Block Storage Disk Encryption is not currently available to all users.", + Computed: true, + }, } var frameworkDataSourceSchema = schema.Schema{ diff --git a/linode/volume/framework_schema_resource.go b/linode/volume/framework_schema_resource.go index db1b5d93c..aa297fcf5 100644 --- a/linode/volume/framework_schema_resource.go +++ b/linode/volume/framework_schema_resource.go @@ -3,6 +3,8 @@ package volume import ( "context" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" @@ -111,5 +113,15 @@ var frameworkResourceSchema = schema.Schema{ }, Default: helper.EmptySetDefault(types.StringType), }, + "encryption": schema.StringAttribute{ + Description: "Whether Block Storage Disk Encryption is enabled or disabled on this Volume. " + + "Note: Block Storage Disk Encryption is not currently available to all users.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("disabled"), + Validators: []validator.String{ + stringvalidator.OneOf("enabled", "disabled"), + }, + }, }, } diff --git a/linode/volume/resource_test.go b/linode/volume/resource_test.go index c117f4f6b..4cf2e4094 100644 --- a/linode/volume/resource_test.go +++ b/linode/volume/resource_test.go @@ -56,6 +56,19 @@ func sweep(prefix string) error { return nil } +func TestSmokeTests_volume(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccResourceVolume_basic_smoke", TestAccResourceVolume_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccResourceVolume_basic_smoke(t *testing.T) { t.Parallel() diff --git a/linode/volume/tmpl/data_with_block_storage_encryption.gotf b/linode/volume/tmpl/data_with_block_storage_encryption.gotf new file mode 100644 index 000000000..44bbf3bc1 --- /dev/null +++ b/linode/volume/tmpl/data_with_block_storage_encryption.gotf @@ -0,0 +1,14 @@ +{{ define "volume_data_with_block_storage_encryption" }} + +resource "linode_volume" "foobar" { + label = "{{.Label}}" + region = "{{ .Region }}" + tags = ["tf_test"] + encryption = "enabled" +} + +data "linode_volume" "foobar" { + id = "${linode_volume.foobar.id}" +} + +{{ end }} \ No newline at end of file diff --git a/linode/volume/tmpl/template.go b/linode/volume/tmpl/template.go index 5ca1da350..accda98a7 100644 --- a/linode/volume/tmpl/template.go +++ b/linode/volume/tmpl/template.go @@ -64,3 +64,8 @@ func DataBasic(t *testing.T, volume, region string) string { return acceptance.ExecuteTemplate(t, "volume_data_basic", TemplateData{Label: volume, Region: region}) } + +func DataWithBlockStorageEncryption(t *testing.T, volume, region string) string { + return acceptance.ExecuteTemplate(t, + "volume_data_with_block_storage_encryption", TemplateData{Label: volume, Region: region}) +} diff --git a/linode/volumetypes/datasource_test.go b/linode/volumetypes/datasource_test.go new file mode 100644 index 000000000..ab9a17285 --- /dev/null +++ b/linode/volumetypes/datasource_test.go @@ -0,0 +1,38 @@ +//go:build integration || volumetypes + +package volumetypes_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" + "github.com/linode/terraform-provider-linode/v2/linode/volumetypes/tmpl" +) + +func TestAccDataSourceVolumeTypes_basic(t *testing.T) { + t.Parallel() + + dataSourceName := "data.linode_volume_types.foobar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: tmpl.DataBasic(t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "types.#", "1"), + resource.TestCheckResourceAttr(dataSourceName, "types.0.id", "volume"), + resource.TestCheckResourceAttr(dataSourceName, "types.0.label", "Storage Volume"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.transfer"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.price.0.hourly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.price.0.monthly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.id"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.hourly"), + resource.TestCheckResourceAttrSet(dataSourceName, "types.0.region_prices.0.monthly"), + ), + }, + }, + }) +} diff --git a/linode/volumetypes/framework_datasource.go b/linode/volumetypes/framework_datasource.go new file mode 100644 index 000000000..f08ecb3b6 --- /dev/null +++ b/linode/volumetypes/framework_datasource.go @@ -0,0 +1,73 @@ +package volumetypes + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v2/linode/helper" +) + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_volume_types", + Schema: &frameworkDataSourceSchema, + }, + ), + } +} + +type DataSource struct { + helper.BaseDataSource +} + +func (r *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data.linode_volume_types") + + var data VolumeTypeFilterModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id, d := filterConfig.GenerateID(data.Filters) + if d != nil { + resp.Diagnostics.Append(d) + return + } + data.ID = id + + result, d := filterConfig.GetAndFilter( + ctx, r.Meta.Client, data.Filters, listVolumeTypes, data.Order, data.OrderBy) + if d != nil { + resp.Diagnostics.Append(d) + return + } + + data.parseVolumeTypes(helper.AnySliceToTyped[linodego.VolumeType](result)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func listVolumeTypes(ctx context.Context, client *linodego.Client, filter string) ([]any, error) { + tflog.Debug(ctx, "Listing Volume types", map[string]any{ + "filter_header": filter, + }) + + types, err := client.ListVolumeTypes(ctx, &linodego.ListOptions{ + Filter: filter, + }) + if err != nil { + return nil, err + } + + return helper.TypedSliceToAny(types), nil +} diff --git a/linode/volumetypes/framework_datasource_schema.go b/linode/volumetypes/framework_datasource_schema.go new file mode 100644 index 000000000..71d5bd3ad --- /dev/null +++ b/linode/volumetypes/framework_datasource_schema.go @@ -0,0 +1,34 @@ +package volumetypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/linode/terraform-provider-linode/v2/linode/helper/frameworkfilter" +) + +var volumeTypeSchema = schema.NestedBlockObject{ + Attributes: helper.GetPricingTypeAttributes("Volume Type"), +} + +var filterConfig = frameworkfilter.Config{ + "label": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeString}, + "transfer": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeInt}, +} + +var frameworkDataSourceSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The data source's unique ID.", + Computed: true, + }, + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), + }, + Blocks: map[string]schema.Block{ + "filter": filterConfig.Schema(), + "types": schema.ListNestedBlock{ + Description: "The returned list of Volume types.", + NestedObject: volumeTypeSchema, + }, + }, +} diff --git a/linode/volumetypes/framework_models.go b/linode/volumetypes/framework_models.go new file mode 100644 index 000000000..07a8afed2 --- /dev/null +++ b/linode/volumetypes/framework_models.go @@ -0,0 +1,121 @@ +package volumetypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v2/linode/helper" + "github.com/linode/terraform-provider-linode/v2/linode/helper/frameworkfilter" +) + +type DataSourceModel struct { + ID types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` + Price types.List `tfsdk:"price"` + RegionPrices types.List `tfsdk:"region_prices"` + Transfer types.Int64 `tfsdk:"transfer"` +} + +func (data *DataSourceModel) parseVolumeType(volumeType *linodego.VolumeType, +) diag.Diagnostics { + data.ID = types.StringValue(volumeType.ID) + + price, diags := flattenPrice(volumeType.Price) + if diags.HasError() { + return diags + } + data.Price = *price + + data.Label = types.StringValue(volumeType.Label) + + regionPrices, d := flattenRegionPrices(volumeType.RegionPrices) + if d.HasError() { + return d + } + data.RegionPrices = *regionPrices + + data.Transfer = types.Int64Value(int64(volumeType.Transfer)) + + return nil +} + +func flattenPrice(price linodego.VolumeTypePrice) ( + *basetypes.ListValue, diag.Diagnostics, +) { + result := make(map[string]attr.Value) + + result["hourly"] = types.Float64Value(float64(price.Hourly)) + result["monthly"] = types.Float64Value(float64(price.Monthly)) + + obj, diag := types.ObjectValue(helper.PriceObjectType.AttrTypes, result) + if diag.HasError() { + return nil, diag + } + + objList := []attr.Value{obj} + + resultList, diag := types.ListValue( + helper.PriceObjectType, + objList, + ) + if diag.HasError() { + return nil, diag + } + + return &resultList, nil +} + +func flattenRegionPrices(prices []linodego.VolumeTypeRegionPrice) ( + *basetypes.ListValue, diag.Diagnostics, +) { + result := make([]attr.Value, len(prices)) + + for i, price := range prices { + obj, d := types.ObjectValue(helper.RegionPriceObjectType.AttrTypes, map[string]attr.Value{ + "id": types.StringValue(price.ID), + "hourly": types.Float64Value(float64(price.Hourly)), + "monthly": types.Float64Value(float64(price.Monthly)), + }) + if d.HasError() { + return nil, d + } + + result[i] = obj + } + + priceList, d := basetypes.NewListValue( + helper.RegionPriceObjectType, + result, + ) + return &priceList, d +} + +type VolumeTypeFilterModel struct { + ID types.String `tfsdk:"id"` + Order types.String `tfsdk:"order"` + OrderBy types.String `tfsdk:"order_by"` + Filters frameworkfilter.FiltersModelType `tfsdk:"filter"` + Types []DataSourceModel `tfsdk:"types"` +} + +func (model *VolumeTypeFilterModel) parseVolumeTypes(volumeTypes []linodego.VolumeType, +) diag.Diagnostics { + result := make([]DataSourceModel, len(volumeTypes)) + + for i := range volumeTypes { + var m DataSourceModel + + diags := m.parseVolumeType(&volumeTypes[i]) + if diags.HasError() { + return diags + } + + result[i] = m + } + + model.Types = result + + return nil +} diff --git a/linode/volumetypes/tmpl/data_basic.gotf b/linode/volumetypes/tmpl/data_basic.gotf new file mode 100644 index 000000000..341fba2c9 --- /dev/null +++ b/linode/volumetypes/tmpl/data_basic.gotf @@ -0,0 +1,10 @@ +{{ define "volume_types_data_basic" }} + +data "linode_volume_types" "foobar" { + filter { + name = "label" + values = ["Storage Volume"] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/volumetypes/tmpl/template.go b/linode/volumetypes/tmpl/template.go new file mode 100644 index 000000000..2ff0f75fe --- /dev/null +++ b/linode/volumetypes/tmpl/template.go @@ -0,0 +1,12 @@ +package tmpl + +import ( + "testing" + + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" +) + +func DataBasic(t *testing.T) string { + return acceptance.ExecuteTemplate(t, + "volume_types_data_basic", nil) +} diff --git a/linode/vpcsubnets/datasource_test.go b/linode/vpcsubnets/datasource_test.go index 8d1b6f249..91c01e632 100644 --- a/linode/vpcsubnets/datasource_test.go +++ b/linode/vpcsubnets/datasource_test.go @@ -13,6 +13,19 @@ import ( "github.com/linode/terraform-provider-linode/v2/linode/vpcsubnets/tmpl" ) +func TestSmokeTests_vpcsubnets(t *testing.T) { + tests := []struct { + name string + test func(*testing.T) + }{ + {"TestAccDataSourceVPCSubnets_basic_smoke", TestAccDataSourceVPCSubnets_basic_smoke}, + } + + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + func TestAccDataSourceVPCSubnets_basic_smoke(t *testing.T) { t.Parallel()