diff --git a/.jfrog-pipelines/TFproviderTest.yml b/.jfrog-pipelines/TFproviderTest.yml index d261bf87..efc48a4a 100644 --- a/.jfrog-pipelines/TFproviderTest.yml +++ b/.jfrog-pipelines/TFproviderTest.yml @@ -14,10 +14,12 @@ resources: pipelines: - name: tf_provider_artifactory steps: - - name: build_and_run_tf_provider - type: Bash + - name: build_and_run_tf_provider_matrix + type: Matrix + stepMode: Bash configuration: #nodePool: default + multiNode: true priority: 1 timeoutSeconds: 3600 # 60 minutes runtime: @@ -47,6 +49,25 @@ pipelines: SONAR_SCANNER_VERSION: 4.7.0.2747 SONAR_SCANNER_HOME: $HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux SONAR_SCANNER_OPTS: "-server" + stepletMultipliers: + environmentVariables: + - directory: ./pkg/artifactory/datasource + - directory: ./pkg/artifactory/datasource/repository/federated + - directory: ./pkg/artifactory/datasource/repository/local + - directory: ./pkg/artifactory/datasource/repository/remote + - directory: ./pkg/artifactory/datasource/repository/virtual + - directory: ./pkg/artifactory/datasource/security + - directory: ./pkg/artifactory/datasource/user + - directory: ./pkg/artifactory/provider + - directory: ./pkg/artifactory/resource/configuration + - directory: ./pkg/artifactory/resource/replication + - directory: ./pkg/artifactory/resource/repository/federated + - directory: ./pkg/artifactory/resource/repository/local + - directory: ./pkg/artifactory/resource/repository/remote + - directory: ./pkg/artifactory/resource/repository/virtual + - directory: ./pkg/artifactory/resource/security + - directory: ./pkg/artifactory/resource/user + - directory: ./pkg/artifactory/resource/webhook execution: onStart: - echo "Sending status to GitHub." @@ -72,11 +93,8 @@ pipelines: - echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list - sudo apt update - sudo apt install goreleaser - # TODO: enable after migration to repo21 - #- echo "Install SonarQube" - #- curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip - #- unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/ onExecute: + - echo "Executing matrix step on ${steplet_id}" - add_run_variables TFProviderRepo=$(echo ${res_GitHubTFProviderRepoJFrog_gitRepoRepositorySshUrl} | sed -e 's/git@/@/g' -e 's/:/\//g') - cd ${res_GitHubTFProviderRepoJFrog_resourcePath} # we need to manually move into the resource path - echo "Verify the code contents merged feature branch with master branch (detached mode)" @@ -106,23 +124,27 @@ pipelines: - export ARTIFACTORY_CONTAINER_NAME=artifactory - >- docker run -i --name ${ARTIFACTORY_CONTAINER_NAME} -t -d --rm -v "${res_GitHubTFProviderRepoJFrog_resourcePath}/scripts/artifactory.lic:/artifactory_extra_conf/artifactory.lic:ro" \ - -p8081:8081 -p8082:8082 -p8080:8080 releases-docker.jfrog.io/jfrog/artifactory-pro:${ARTIFACTORY_VERSION} + -p 8082:8082 releases-docker.jfrog.io/jfrog/artifactory-pro:${ARTIFACTORY_VERSION} - echo "Set localhost to a container IP address, since we run docker inside of docker" - export LOCALHOST=$(docker inspect -f '{{`{{range.NetworkSettings.Networks}}{{.Gateway}}{{end}}`}}' ${ARTIFACTORY_CONTAINER_NAME}) - echo "Using ${LOCALHOST} as 'localhost' ip address" - echo "Waiting for Artifactory to start (doesn't reflect the start of the UI!)" + - export ARTIFACTORY_URL="http://${LOCALHOST}:8082" - >- - until curl -sf -u admin:password http://${LOCALHOST}:8081/artifactory/api/system/licenses/; do + until curl -sf -u admin:password ${ARTIFACTORY_URL}/artifactory/api/system/licenses/; do printf '.' sleep 4 done - echo "Add variables needed to run Terraform Provider" - - export ARTIFACTORY_URL="http://${LOCALHOST}:8082" - export ARTIFACTORY_USERNAME=admin - export ARTIFACTORY_PASSWORD=password - echo "Get cookie to generate Access token. We need a pause to let UI come up to get cookies" - - sleep 180 - - sudo curl http://${LOCALHOST}:8082/router/api/v1/system/health + - >- + until curl -sf -u admin:password ${ARTIFACTORY_URL}/ui/login/; do + printf '.' + sleep 4 + done + - sudo curl ${ARTIFACTORY_URL}/router/api/v1/system/health - >- export COOKIES=$(curl -c - "${ARTIFACTORY_URL}/ui/api/v1/ui/auth/login?_spring_security_remember_me=false" \ --header "accept: application/json, text/plain, */*" \ @@ -139,22 +161,18 @@ pipelines: - add_run_variables ARTIFACTORY_ACCESS_TOKEN=${ACCESS_KEY} - echo "Unset ARTIFACTORY_PASSWORD, acceptance test will use ARTIFACTORY_ACCESS_TOKEN instead" - unset ARTIFACTORY_PASSWORD - - export TF_ACC=true - - make acceptance - # TODO: enable after migration to repo21 - #- export SONAR_TOKEN=${int_terraform_artifactory_sonarqube_token} - #- make scan + - TF_ACC=true make acceptance TEST=${directory} - make install onSuccess: - echo "Success" - - send_notification partnership_slack --text "${pipeline_name} step <${step_url}|${step_name}> is completed. Version ${PROVIDER_VERSION:-" wasn't set"}." + - send_notification partnership_slack --text "${pipeline_name} step <${step_url}|${step_name} - ${directory}> is completed. Version ${PROVIDER_VERSION:-" wasn't set"}." onFailure: - echo "Failure, sending status to GitHub and Slack." - export STATE="failure" - export DESCRIPTION="Pipeline has failed." - git clone https://${int_partnership_github_token}@github.com/jfrog/terraform-provider-shared.git - ./terraform-provider-shared/scripts/github-status.sh ${res_GitHubTFProviderRepoJFrog_gitProvider_token} ${res_GitHubTFProviderRepoJFrog_gitRepoFullName} ${res_GitHubTFProviderRepoJFrog_commitSha} - - send_notification partnership_slack --text "${pipeline_name} pipeline failed on <${step_url}|${step_name}> step" + - send_notification partnership_slack --text "${pipeline_name} pipeline failed on <${step_url}|${step_name} - ${directory}> step" onComplete: - echo "Complete" @@ -167,7 +185,7 @@ pipelines: - name: partnership_slack - name: partnership_github inputSteps: - - name: build_and_run_tf_provider + - name: build_and_run_tf_provider_matrix inputResources: - name: GitHubTFProviderRepoJFrog execution: diff --git a/CHANGELOG.md b/CHANGELOG.md index 81a26b86..ad969173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 9.4.0 (Oct 5, 2023). Tested on Artifactory 7.68.13 with Terraform CLI v1.6.0 + +FEATURES: + +* resource/artifactory_mail_server: add a new resource for managing mail server configuration. PR: [#819](https://github.com/jfrog/terraform-provider-artifactory/pull/819) Issue: [#735](https://github.com/jfrog/terraform-provider-artifactory/issues/735) + +## 9.3.1 (Oct 6, 2023). Tested on Artifactory 7.68.13 with Terraform CLI v1.6.0 + +BUG FIX: +* resource/artifactory_scoped_token: Remove default value for `expires_in` attribute which should fix state drift when upgrading from 7.11.2 or earlier. Issue: [#818](https://github.com/jfrog/terraform-provider-artifactory/issues/818) PR: [#820](https://github.com/jfrog/terraform-provider-artifactory/pull/820) + ## 9.3.0 (Oct 3, 2023). Tested on Artifactory 7.68.13 with Terraform CLI v1.5.7 IMPROVEMENTS: @@ -5,7 +16,6 @@ IMPROVEMENTS: * resource/artifactory_distribution_public_key is migrated to Plugin Framework. PR: [#817](https://github.com/jfrog/terraform-provider-artifactory/pull/817) * resource/artifactory_remote_\*\_repository: Fix incorrect default value for `store_artifacts_locally` attribute in documentation. PR: [#816](https://github.com/jfrog/terraform-provider-artifactory/pull/816) - ## 9.2.1 (Sep 29, 2023). Tested on Artifactory 7.68.11 with Terraform CLI v1.5.7 IMPROVEMENTS: diff --git a/GNUmakefile b/GNUmakefile index de67bd32..7616841a 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,4 +1,4 @@ -TEST?=./... +TEST?=./pkg/... PRODUCT=artifactory GO_ARCH=$(shell go env GOARCH) TARGET_ARCH=$(shell go env GOOS)_${GO_ARCH} @@ -52,11 +52,11 @@ attach: smoke: fmt export TF_ACC=true && \ - go test -run '${SMOKE_TESTS}' -ldflags="-X '${PKG_VERSION_PATH}.Version=${NEXT_PROVIDER_VERSION}-test'" -v -p 1 -timeout 5m ./pkg/... -count=1 + go test -run '${SMOKE_TESTS}' -ldflags="-X '${PKG_VERSION_PATH}.Version=${NEXT_PROVIDER_VERSION}-test'" -v -p 1 -timeout 5m $(TEST). -count=1 acceptance: fmt export TF_ACC=true && \ - go test -cover -coverprofile=coverage.txt -ldflags="-X '${PKG_VERSION_PATH}.Version=${NEXT_PROVIDER_VERSION}-test'" -v -p 1 -parallel 20 -timeout 1h ./pkg/... + go test -cover -coverprofile=coverage.txt -ldflags="-X '${PKG_VERSION_PATH}.Version=${NEXT_PROVIDER_VERSION}-test'" -v -p 1 -parallel 20 -timeout 1h $(TEST) coverage: go tool cover -html=coverage.txt diff --git a/docs/resources/mail_server.md b/docs/resources/mail_server.md new file mode 100644 index 00000000..1d1b0ee0 --- /dev/null +++ b/docs/resources/mail_server.md @@ -0,0 +1,57 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "artifactory_mail_server Resource - terraform-provider-artifactory" +subcategory: "Configuration" +--- + +# Artifactory Mail Server Resource + +Provides an Artifactory Mail Server resource. This can be used to create and manage Artifactory mail server configuration. + +## Example Usages + +```terraform +resource "artifactory_mail_server" "mymailserver" { + enabled = true + artifactory_url = "http://tempurl.org" + from = "test@jfrog.com" + host = "http://tempurl.org" + username = "test-user" + password = "test-password" + port = 25 + subject_prefix = "[Test]" + use_ssl = true + use_tls = true +} +``` + +## Argument reference + + +## Schema + +### Required + +- `enabled` (Boolean) When set, mail notifications are enabled. +- `host` (String) The mail server IP address / DNS. +- `port` (Number) The port number of the mail server. + +### Optional + +- `artifactory_url` (String) The Artifactory URL to to link to in all outgoing messages. +- `from` (String) The 'from' address header to use in all outgoing messages. +- `password` (String) The password for authentication with the mail server. +- `subject_prefix` (String) A prefix to use for the subject of all outgoing mails. +- `use_ssl` (Boolean) When set to 'true', uses a secure connection to the mail server. +- `use_tls` (Boolean) When set to 'true', uses Transport Layer Security when connecting to the mail server. +- `username` (String) The username for authentication with the mail server. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import artifactory_mail_server.my-mail-server mymailserver +``` + +~>The `password` attribute is not retrievable from Artifactory thus there will be state drift after importing this resource. diff --git a/docs/resources/scoped_token.md b/docs/resources/scoped_token.md index 0784e653..da3ac117 100644 --- a/docs/resources/scoped_token.md +++ b/docs/resources/scoped_token.md @@ -75,7 +75,7 @@ resource "artifactory_scoped_token" "audience" { - `audiences` (Set of String) A list of the other instances or services that should accept this token identified by their Service-IDs. Limited to total 255 characters. Default to '*@*' if not set. Service ID must begin with valid JFrog service type. Options: jfrt, jfxr, jfpip, jfds, jfmc, jfac, jfevt, jfmd, jfcon, or *. For instructions to retrieve the Artifactory Service ID see this [documentation](https://jfrog.com/help/r/jfrog-rest-apis/get-service-id) - `description` (String) Free text token description. Useful for filtering and managing tokens. Limited to 1024 characters. -- `expires_in` (Number) The amount of time, in seconds, it would take for the token to expire. An admin shall be able to set whether expiry is mandatory, what is the default expiry, and what is the maximum expiry allowed. Must be non-negative. Default value is based on configuration in 'access.config.yaml'. See [API documentation](https://jfrog.com/help/r/jfrog-rest-apis/revoke-token-by-id) for details. Access Token would not be saved by Artifactory if this is less than the persistence threshold value (default to 10800 seconds) set in Access configuration. See [official documentation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/using-the-revocable-and-persistency-thresholds) for details. +- `expires_in` (Number) The amount of time, in seconds, it would take for the token to expire. An admin shall be able to set whether expiry is mandatory, what is the default expiry, and what is the maximum expiry allowed. Must be non-negative. Default value is based on configuration in 'access.config.yaml'. See [API documentation](https://jfrog.com/help/r/jfrog-rest-apis/create-token) for details. Access Token would not be saved by Artifactory if this is less than the persistence threshold value (default to 10800 seconds) set in Access configuration. See [official documentation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/using-the-revocable-and-persistency-thresholds) for details. - `grant_type` (String) The grant type used to authenticate the request. In this case, the only value supported is `client_credentials` which is also the default value if this parameter is not specified. - `include_reference_token` (Boolean) Also create a reference token which can be used like an API key. - `refreshable` (Boolean) Is this token refreshable? Default is `false`. diff --git a/examples/resources/artifactory_mail_server/import.sh b/examples/resources/artifactory_mail_server/import.sh new file mode 100644 index 00000000..46fe0436 --- /dev/null +++ b/examples/resources/artifactory_mail_server/import.sh @@ -0,0 +1 @@ +terraform import artifactory_mail_server.my-mail-server mymailserver \ No newline at end of file diff --git a/examples/resources/artifactory_mail_server/resource.tf b/examples/resources/artifactory_mail_server/resource.tf new file mode 100644 index 00000000..4389f6ad --- /dev/null +++ b/examples/resources/artifactory_mail_server/resource.tf @@ -0,0 +1,12 @@ +resource "artifactory_mail_server" "mymailserver" { + enabled = true + artifactory_url = "http://tempurl.org" + from = "test@jfrog.com" + host = "http://tempurl.org" + username = "test-user" + password = "test-password" + port = 25 + subject_prefix = "[Test]" + use_ssl = true + use_tls = true +} \ No newline at end of file diff --git a/go.mod b/go.mod index 88e07c70..b49b3259 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,13 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/terraform-plugin-docs v0.16.0 github.com/hashicorp/terraform-plugin-framework v1.3.5 - github.com/hashicorp/terraform-plugin-framework-validators v0.10.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 github.com/hashicorp/terraform-plugin-go v0.18.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0 github.com/hashicorp/terraform-plugin-testing v1.5.1 - github.com/jfrog/terraform-provider-shared v1.19.0 + github.com/jfrog/terraform-provider-shared v1.20.0 github.com/sethvargo/go-password v0.2.0 github.com/stretchr/testify v1.7.2 golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 diff --git a/go.sum b/go.sum index 5c825af4..dbaa09d9 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFcc github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA= github.com/hashicorp/terraform-plugin-framework v1.3.5 h1:FJ6s3CVWVAxlhiF/jhy6hzs4AnPHiflsp9KgzTGl1wo= github.com/hashicorp/terraform-plugin-framework v1.3.5/go.mod h1:2gGDpWiTI0irr9NSTLFAKlTi6KwGti3AoU19rFqU30o= -github.com/hashicorp/terraform-plugin-framework-validators v0.10.0 h1:4L0tmy/8esP6OcvocVymw52lY0HyQ5OxB7VNl7k4bS0= -github.com/hashicorp/terraform-plugin-framework-validators v0.10.0/go.mod h1:qdQJCdimB9JeX2YwOpItEu+IrfoJjWQ5PhLpAOMDQAE= +github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= +github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg= github.com/hashicorp/terraform-plugin-go v0.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE= github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= @@ -116,8 +116,8 @@ github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jfrog/terraform-provider-shared v1.19.0 h1:4/CgvSTqhf00eHMo8q+xL2/N8Lng7T+Pw5GbNSUwxRs= -github.com/jfrog/terraform-provider-shared v1.19.0/go.mod h1:JvTKRAXMQyX6gQjESY+YK2lJLeW8uKTVHar5HDTnvp0= +github.com/jfrog/terraform-provider-shared v1.20.0 h1:T5AFbn4Su3tlNZTIXwb8Bi4vq/LZMFH312V2z8d3IsI= +github.com/jfrog/terraform-provider-shared v1.20.0/go.mod h1:KEYnVOggRuQT6qLR05ra0QfQa0SeYnkMnN0ZqIgQHqI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index d1a6821d..f32aea08 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -127,7 +127,7 @@ func (p *ArtifactoryProvider) Configure(ctx context.Context, req provider.Config fmt.Sprintf("%v", err), ) } - if config.CheckLicense.IsNull() || config.CheckLicense.ValueBool() == true { + if config.CheckLicense.IsNull() || config.CheckLicense.ValueBool() { licenseErr := utilsdk.CheckArtifactoryLicense(restyBase, "Enterprise", "Commercial", "Edge") if licenseErr != nil { resp.Diagnostics.AddError( @@ -159,7 +159,6 @@ func (p *ArtifactoryProvider) Configure(ctx context.Context, req provider.Config Client: restyBase, ArtifactoryVersion: version, } - } // Resources satisfies the provider.Provider interface for ArtifactoryProvider. @@ -176,6 +175,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R configuration.NewLdapSettingResource, configuration.NewLdapGroupSettingResource, configuration.NewBackupResource, + configuration.NewMailServerResource, } } diff --git a/pkg/artifactory/provider/provider.go b/pkg/artifactory/provider/provider.go new file mode 100644 index 00000000..b6841db5 --- /dev/null +++ b/pkg/artifactory/provider/provider.go @@ -0,0 +1,4 @@ +package provider + +var Version = "9.0.0" // needs to be exported so make file can update this +var productId = "terraform-provider-artifactory/" + Version diff --git a/pkg/artifactory/provider/sdkv2.go b/pkg/artifactory/provider/sdkv2.go index 5428e958..a651c202 100644 --- a/pkg/artifactory/provider/sdkv2.go +++ b/pkg/artifactory/provider/sdkv2.go @@ -13,9 +13,6 @@ import ( "github.com/jfrog/terraform-provider-shared/validator" ) -var Version = "7.0.0" // needs to be exported so make file can update this -var productId = "terraform-provider-artifactory/" + Version - // SdkV2 Artifactory provider that supports configuration via Access Token // Supported resources are repos, users, groups, replications, and permissions func SdkV2() *schema.Provider { diff --git a/pkg/artifactory/resource/configuration/resource_artifactory_backup.go b/pkg/artifactory/resource/configuration/resource_artifactory_backup.go index 1f957445..e76e3e2b 100644 --- a/pkg/artifactory/resource/configuration/resource_artifactory_backup.go +++ b/pkg/artifactory/resource/configuration/resource_artifactory_backup.go @@ -18,7 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" utilfw "github.com/jfrog/terraform-provider-shared/util/fw" utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" - validatorfw "github.com/jfrog/terraform-provider-shared/validator/fw" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" "gopkg.in/yaml.v3" ) @@ -136,7 +136,7 @@ func (r *BackupResource) Schema(ctx context.Context, req resource.SchemaRequest, MarkdownDescription: "Cron expression to control the backup frequency.", Required: true, Validators: []validator.String{ - validatorfw.IsCron(), + validatorfw_string.IsCron(), }, }, "retention_period_hours": schema.Int64Attribute{ diff --git a/pkg/artifactory/resource/configuration/resource_artifactory_backup_test.go b/pkg/artifactory/resource/configuration/resource_artifactory_backup_test.go index 36f4bafa..5d744a0a 100644 --- a/pkg/artifactory/resource/configuration/resource_artifactory_backup_test.go +++ b/pkg/artifactory/resource/configuration/resource_artifactory_backup_test.go @@ -106,7 +106,7 @@ func TestAccBackup_importNotFound(t *testing.T) { ` resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: config, @@ -134,12 +134,12 @@ func TestAccBackup_invalid_cron(t *testing.T) { ` resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: config, ResourceName: "artifactory_backup.invalid-cron-test", - ExpectError: regexp.MustCompile("value must match be a valid cron expression"), + ExpectError: regexp.MustCompile("value must be a valid cron expression"), }, }, }) @@ -180,7 +180,7 @@ func cronTestCase(cronExpression string, t *testing.T) (*testing.T, resource.Tes return t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: acctest.VerifyDeleted(fqrn, acctest.CheckRepo), Steps: []resource.TestStep{ { diff --git a/pkg/artifactory/resource/configuration/resource_artifactory_mail_server.go b/pkg/artifactory/resource/configuration/resource_artifactory_mail_server.go new file mode 100644 index 00000000..92f2f87f --- /dev/null +++ b/pkg/artifactory/resource/configuration/resource_artifactory_mail_server.go @@ -0,0 +1,340 @@ +package configuration + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "gopkg.in/yaml.v3" +) + +type MailServerAPIModel struct { + Enabled bool `xml:"enabled" yaml:"enabled"` + ArtifactoryURL string `xml:"artifactoryUrl" yaml:"artifactoryUrl"` + From string `xml:"from" yaml:"from"` + Host string `xml:"host" yaml:"host"` + Username string `xml:"username" yaml:"username"` + Password string `xml:"password" yaml:"password"` + Port int64 `xml:"port" yaml:"port"` + SubjectPrefix string `xml:"subjectPrefix" yaml:"subjectPrefix"` + UseSSL bool `xml:"ssl" yaml:"ssl"` + UseTLS bool `xml:"tls" yaml:"tls"` +} + +type MailServer struct { + Server *MailServerAPIModel `xml:"mailServer"` +} + +type MailServerResourceModel struct { + Enabled types.Bool `tfsdk:"enabled"` + ArtifactoryURL types.String `tfsdk:"artifactory_url"` + From types.String `tfsdk:"from"` + Host types.String `tfsdk:"host"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + Port types.Int64 `tfsdk:"port"` + SubjectPrefix types.String `tfsdk:"subject_prefix"` + UseSSL types.Bool `tfsdk:"use_ssl"` + UseTLS types.Bool `tfsdk:"use_tls"` +} + +func (r *MailServerResourceModel) ToAPIModel(ctx context.Context, mailServer *MailServerAPIModel) diag.Diagnostics { + // Convert from Terraform resource model into API model + *mailServer = MailServerAPIModel{ + Enabled: r.Enabled.ValueBool(), + ArtifactoryURL: r.ArtifactoryURL.ValueString(), + From: r.From.ValueString(), + Host: r.Host.ValueString(), + Username: r.Username.ValueString(), + Password: r.Password.ValueString(), + Port: r.Port.ValueInt64(), + SubjectPrefix: r.SubjectPrefix.ValueString(), + UseSSL: r.UseSSL.ValueBool(), + UseTLS: r.UseTLS.ValueBool(), + } + + return nil +} + +func (r *MailServerResourceModel) FromAPIModel(ctx context.Context, mailServer *MailServerAPIModel) diag.Diagnostics { + r.Enabled = types.BoolValue(mailServer.Enabled) + r.ArtifactoryURL = types.StringValue(mailServer.ArtifactoryURL) + r.From = types.StringValue(mailServer.From) + r.Host = types.StringValue(mailServer.Host) + r.Username = types.StringValue(mailServer.Username) + r.Port = types.Int64Value(mailServer.Port) + r.SubjectPrefix = types.StringValue(mailServer.SubjectPrefix) + r.UseSSL = types.BoolValue(mailServer.UseSSL) + r.UseTLS = types.BoolValue(mailServer.UseTLS) + + return nil +} + +func NewMailServerResource() resource.Resource { + return &MailServerResource{} +} + +type MailServerResource struct { + ProviderData utilsdk.ProvderMetadata +} + +func (r *MailServerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "artifactory_mail_server" +} + +func (r *MailServerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Provides an Artifactory mail server config resource. This resource configuration corresponds to mail server config block in system configuration XML (REST endpoint: artifactory/api/system/configuration). Manages mail server settings of the Artifactory instance.", + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + MarkdownDescription: "When set, mail notifications are enabled.", + Required: true, + }, + "artifactory_url": schema.StringAttribute{ + MarkdownDescription: "The Artifactory URL to to link to in all outgoing messages.", + Optional: true, + Validators: []validator.String{ + validatorfw_string.IsURLHttpOrHttps(), + }, + }, + "from": schema.StringAttribute{ + MarkdownDescription: "The 'from' address header to use in all outgoing messages.", + Optional: true, + Validators: []validator.String{ + validatorfw_string.IsEmail(), + }, + }, + "host": schema.StringAttribute{ + MarkdownDescription: "The mail server IP address / DNS.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "username": schema.StringAttribute{ + MarkdownDescription: "The username for authentication with the mail server.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "password": schema.StringAttribute{ + MarkdownDescription: "The password for authentication with the mail server.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "port": schema.Int64Attribute{ + MarkdownDescription: "The port number of the mail server.", + Required: true, + Validators: []validator.Int64{ + int64validator.AtMost(65535), + }, + }, + "subject_prefix": schema.StringAttribute{ + MarkdownDescription: "A prefix to use for the subject of all outgoing mails.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "use_ssl": schema.BoolAttribute{ + MarkdownDescription: "When set to 'true', uses a secure connection to the mail server.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "use_tls": schema.BoolAttribute{ + MarkdownDescription: "When set to 'true', uses Transport Layer Security when connecting to the mail server.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + } +} + +func (r *MailServerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + r.ProviderData = req.ProviderData.(utilsdk.ProvderMetadata) +} + +func (r *MailServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan *MailServerResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var mailServer MailServerAPIModel + resp.Diagnostics.Append(plan.ToAPIModel(ctx, &mailServer)...) + if resp.Diagnostics.HasError() { + return + } + + /* EXPLANATION FOR BELOW CONSTRUCTION USAGE. + + There is a difference in xml structure usage between GET and PATCH calls of API: /artifactory/api/system/configuration. + + GET call structure has "backups -> backup -> Array of backup config blocks". + + PATCH call structure has "backups -> Name/Key of backup that is being patched -> config block of the backup being patched". + + Since the Name/Key is dynamic string, following nested map of string structs are constructed to match the usage of PATCH call. + + See https://www.jfrog.com/confluence/display/JFROG/Artifactory+YAML+Configuration for patching system configuration + using YAML + */ + var constructBody = map[string]MailServerAPIModel{} + constructBody["mailServer"] = mailServer + content, err := yaml.Marshal(&constructBody) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + err = SendConfigurationPatch(content, r.ProviderData) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + // Assign the resource ID for the resource in the state + plan.Host = types.StringValue(mailServer.Host) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MailServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state *MailServerResourceModel + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var mailServer MailServer + _, err := r.ProviderData.Client.R(). + SetResult(&mailServer). + Get("artifactory/api/system/configuration") + if err != nil { + utilfw.UnableToRefreshResourceError(resp, "failed to retrieve data from API: /artifactory/api/system/configuration during Read") + return + } + + if mailServer.Server == nil { + resp.Diagnostics.AddAttributeWarning( + path.Root("host"), + "no mail server found", + "", + ) + resp.State.RemoveResource(ctx) + return + } + + // Convert from the API data model to the Terraform data model + // and refresh any attribute values. + resp.Diagnostics.Append(state.FromAPIModel(ctx, mailServer.Server)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MailServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan *MailServerResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + // Convert from Terraform data model into API data model + var mailServer MailServerAPIModel + resp.Diagnostics.Append(plan.ToAPIModel(ctx, &mailServer)...) + + /* EXPLANATION FOR BELOW CONSTRUCTION USAGE. + + There is a difference in xml structure usage between GET and PATCH calls of API: /artifactory/api/system/configuration. + + GET call structure has "backups -> backup -> Array of backup config blocks". + + PATCH call structure has "backups -> Name/Key of backup that is being patched -> config block of the backup being patched". + + Since the Name/Key is dynamic string, following nested map of string structs are constructed to match the usage of PATCH call. + + See https://www.jfrog.com/confluence/display/JFROG/Artifactory+YAML+Configuration for patching system configuration + using YAML + */ + var constructBody = map[string]MailServerAPIModel{} + constructBody["mailServer"] = mailServer + content, err := yaml.Marshal(&constructBody) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + err = SendConfigurationPatch(content, r.ProviderData) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + resp.Diagnostics.Append(plan.FromAPIModel(ctx, &mailServer)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MailServerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MailServerResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + deleteMailServerConfig := `mailServer: ~` + + err := SendConfigurationPatch([]byte(deleteMailServerConfig), r.ProviderData) + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *MailServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // "host" attribute is used here but it's a noop. There's only ever one mail server on Artifactory + // so there's no need to use ID to fetch. + resource.ImportStatePassthroughID(ctx, path.Root("host"), req, resp) +} diff --git a/pkg/artifactory/resource/configuration/resource_artifactory_mail_server_test.go b/pkg/artifactory/resource/configuration/resource_artifactory_mail_server_test.go new file mode 100644 index 00000000..7d792333 --- /dev/null +++ b/pkg/artifactory/resource/configuration/resource_artifactory_mail_server_test.go @@ -0,0 +1,193 @@ +package configuration_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/jfrog/terraform-provider-artifactory/v9/pkg/acctest" + "github.com/jfrog/terraform-provider-artifactory/v9/pkg/artifactory/resource/configuration" + "github.com/jfrog/terraform-provider-shared/testutil" + utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" +) + +func TestAccMailServer_full(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("mailserver-", "artifactory_mail_server") + + const mailServerTemplate = ` + resource "artifactory_mail_server" "{{ .resourceName }}" { + enabled = true + artifactory_url = "{{ .artifactory_url }}" + from = "{{ .from }}" + host = "{{ .host }}" + username = "test-user" + password = "test-password" + port = 25 + subject_prefix = "[Test]" + }` + + testData := map[string]string{ + "resourceName": resourceName, + "artifactory_url": "http://tempurl.org", + "from": "test@jfrog.com", + "host": "http://tempurl.org", + } + + const mailServerTemplateUpdate = ` + resource "artifactory_mail_server" "{{ .resourceName }}" { + enabled = true + artifactory_url = "{{ .artifactory_url }}" + from = "{{ .from }}" + host = "{{ .host }}" + username = "test-user" + password = "test-password" + port = 25 + subject_prefix = "[Test]" + use_ssl = true + use_tls = true + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccMailServerDestroy(resourceName), + + Steps: []resource.TestStep{ + { + Config: utilsdk.ExecuteTemplate(fqrn, mailServerTemplate, testData), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "enabled", "true"), + resource.TestCheckResourceAttr(fqrn, "artifactory_url", testData["artifactory_url"]), + resource.TestCheckResourceAttr(fqrn, "from", testData["from"]), + resource.TestCheckResourceAttr(fqrn, "host", testData["host"]), + resource.TestCheckResourceAttr(fqrn, "username", "test-user"), + resource.TestCheckResourceAttr(fqrn, "password", "test-password"), + resource.TestCheckResourceAttr(fqrn, "port", "25"), + resource.TestCheckResourceAttr(fqrn, "subject_prefix", "[Test]"), + resource.TestCheckResourceAttr(fqrn, "use_ssl", "false"), + resource.TestCheckResourceAttr(fqrn, "use_tls", "false"), + ), + }, + { + Config: utilsdk.ExecuteTemplate(fqrn, mailServerTemplateUpdate, testData), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "enabled", "true"), + resource.TestCheckResourceAttr(fqrn, "artifactory_url", testData["artifactory_url"]), + resource.TestCheckResourceAttr(fqrn, "from", testData["from"]), + resource.TestCheckResourceAttr(fqrn, "host", testData["host"]), + resource.TestCheckResourceAttr(fqrn, "username", "test-user"), + resource.TestCheckResourceAttr(fqrn, "password", "test-password"), + resource.TestCheckResourceAttr(fqrn, "port", "25"), + resource.TestCheckResourceAttr(fqrn, "subject_prefix", "[Test]"), + resource.TestCheckResourceAttr(fqrn, "use_ssl", "true"), + resource.TestCheckResourceAttr(fqrn, "use_tls", "true"), + ), + }, + { + ResourceName: fqrn, + ImportStateId: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "host", + ImportStateVerifyIgnore: []string{"password"}, + }, + }, + }) +} + +func TestAccMailServer_invalid_from(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("mailserver-", "artifactory_mail_server") + + template := ` + resource "artifactory_mail_server" "{{ .resourceName }}" { + enabled = true + artifactory_url = "http://tempurl.org" + from = "invalid-email" + host = "http://tempurl.org" + username = "test-user" + password = "test-password" + port = 25 + subject_prefix = "[Test]" + use_ssl = true + use_tls = true + }` + + testData := map[string]string{ + "resourceName": resourceName, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: utilsdk.ExecuteTemplate(fqrn, template, testData), + ResourceName: resourceName, + ExpectError: regexp.MustCompile("value must be a valid email address"), + }, + }, + }) +} + +func TestAccMailServer_invalid_artifactory_url(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("mailserver-", "artifactory_mail_server") + + template := ` + resource "artifactory_mail_server" "{{ .resourceName }}" { + enabled = true + artifactory_url = "invalid-url" + from = "test-user@jfrog.com" + host = "http://tempurl.org" + username = "test-user" + password = "test-password" + port = 25 + subject_prefix = "[Test]" + use_ssl = true + use_tls = true + }` + + testData := map[string]string{ + "resourceName": resourceName, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: utilsdk.ExecuteTemplate(fqrn, template, testData), + ResourceName: resourceName, + ExpectError: regexp.MustCompile("value must be a valid URL with host.*"), + }, + }, + }) +} + +func testAccMailServerDestroy(id string) func(*terraform.State) error { + return func(s *terraform.State) error { + client := acctest.Provider.Meta().(utilsdk.ProvderMetadata).Client + + _, ok := s.RootModule().Resources["artifactory_mail_server."+id] + if !ok { + return fmt.Errorf("error: resource id [%s] not found", id) + } + + var mailServer configuration.MailServer + + response, err := client.R().SetResult(&mailServer).Get("artifactory/api/system/configuration") + if err != nil { + return err + } + if response.IsError() { + return fmt.Errorf("got error response for API: /artifactory/api/system/configuration request during Read. Response:%#v", response) + } + + if mailServer.Server != nil { + return fmt.Errorf("error: MailServer config still exists.") + } + + return nil + } +} diff --git a/pkg/artifactory/resource/security/resource_artifactory_distribution_public_key_test.go b/pkg/artifactory/resource/security/resource_artifactory_distribution_public_key_test.go index 20690de1..5b2d76f8 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_distribution_public_key_test.go +++ b/pkg/artifactory/resource/security/resource_artifactory_distribution_public_key_test.go @@ -97,7 +97,7 @@ func TestAccDistributionPublicKey_FormatCheck(t *testing.T) { `, resource_name, name, id) resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: keyBasic, @@ -112,7 +112,7 @@ func TestAccDistributionPublicKey_Create(t *testing.T) { keyBasic := fmt.Sprintf(template, resource_name, name, name) resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckDistributionPublicKeyDestroy(fqrn), Steps: []resource.TestStep{ { diff --git a/pkg/artifactory/resource/security/resource_artifactory_permission_target_test.go b/pkg/artifactory/resource/security/resource_artifactory_permission_target_test.go index 82094346..8e79452f 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_permission_target_test.go +++ b/pkg/artifactory/resource/security/resource_artifactory_permission_target_test.go @@ -536,7 +536,7 @@ func TestAccPermissionTarget_MissingRepositories(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testPermissionTargetCheckDestroy(permFqrn), Steps: []resource.TestStep{ { diff --git a/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go b/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go index c41b8bd4..85b159c8 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go +++ b/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go @@ -16,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" @@ -198,7 +197,6 @@ func (r *ScopedTokenResource) Schema(ctx context.Context, req resource.SchemaReq MarkdownDescription: "The amount of time, in seconds, it would take for the token to expire. An admin shall be able to set whether expiry is mandatory, what is the default expiry, and what is the maximum expiry allowed. Must be non-negative. Default value is based on configuration in 'access.config.yaml'. See [API documentation](https://jfrog.com/help/r/jfrog-rest-apis/revoke-token-by-id) for details. Access Token would not be saved by Artifactory if this is less than the persistence threshold value (default to 10800 seconds) set in Access configuration. See [official documentation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/using-the-revocable-and-persistency-thresholds) for details.", Optional: true, Computed: true, - Default: int64default.StaticInt64(0), PlanModifiers: []planmodifier.Int64{ int64planmodifier.RequiresReplaceIfConfigured(), int64planmodifier.UseStateForUnknown(), diff --git a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go index eb879b5d..ac9a067e 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go +++ b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go @@ -91,6 +91,64 @@ func TestAccScopedToken_UpgradeGH_792(t *testing.T) { }) } +func TestAccScopedToken_UpgradeGH_818(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-scope-token", "artifactory_scoped_token") + config := utilsdk.ExecuteTemplate( + "TestAccScopedToken", + `resource "artifactory_user" "test-user" { + name = "testuser" + email = "testuser@tempurl.org" + admin = true + disable_ui_access = false + groups = ["readers"] + password = "Passw0rd!" + } + + resource "artifactory_scoped_token" "{{ .name }}" { + scopes = ["applied-permissions/user"] + username = artifactory_user.test-user.name + }`, + map[string]interface{}{ + "name": name, + }, + ) + + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + VersionConstraint: "7.2.0", + Source: "registry.terraform.io/jfrog/artifactory", + }, + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "username", "testuser"), + resource.TestCheckResourceAttr(fqrn, "scopes.#", "1"), + resource.TestCheckResourceAttr(fqrn, "expires_in", "31536000"), + resource.TestCheckNoResourceAttr(fqrn, "audiences"), + resource.TestCheckResourceAttrSet(fqrn, "access_token"), + resource.TestCheckNoResourceAttr(fqrn, "refresh_token"), + resource.TestCheckNoResourceAttr(fqrn, "reference_token"), + resource.TestCheckResourceAttr(fqrn, "token_type", "Bearer"), + resource.TestCheckResourceAttrSet(fqrn, "subject"), + resource.TestCheckResourceAttrSet(fqrn, "expiry"), + resource.TestCheckResourceAttrSet(fqrn, "issued_at"), + resource.TestCheckResourceAttrSet(fqrn, "issuer"), + ), + ConfigPlanChecks: acctest.ConfigPlanChecks, + }, + { + ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + Config: config, + PlanOnly: true, + ConfigPlanChecks: acctest.ConfigPlanChecks, + }, + }, + }) +} + func scopedTokenUpgradeTestCase(version string, t *testing.T) (*testing.T, resource.TestCase) { _, fqrn, name := testutil.MkNames("test-access-token", "artifactory_scoped_token") @@ -188,7 +246,7 @@ func TestAccScopedToken_WithDefaults(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: acctest.VerifyDeleted(fqrn, security.CheckAccessToken), Steps: []resource.TestStep{ { @@ -246,7 +304,7 @@ func TestAccScopedToken_WithAttributes(t *testing.T) { scopes = ["applied-permissions/admin", "system:metrics:r"] description = "test description" refreshable = true - expires_in = 31536000 + expires_in = 0 audiences = ["jfrt@1", "jfxr@*"] }`, map[string]interface{}{ @@ -260,7 +318,7 @@ func TestAccScopedToken_WithAttributes(t *testing.T) { acctest.PreCheck(t) acctest.CreateProject(t, projectKey) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: acctest.VerifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteProject(t, projectKey) return security.CheckAccessToken(id, request) @@ -275,7 +333,7 @@ func TestAccScopedToken_WithAttributes(t *testing.T) { resource.TestCheckTypeSetElemAttr(fqrn, "scopes.*", "applied-permissions/admin"), resource.TestCheckTypeSetElemAttr(fqrn, "scopes.*", "system:metrics:r"), resource.TestCheckResourceAttr(fqrn, "refreshable", "true"), - resource.TestCheckResourceAttr(fqrn, "expires_in", "31536000"), + resource.TestCheckResourceAttr(fqrn, "expires_in", "0"), resource.TestCheckResourceAttr(fqrn, "description", "test description"), resource.TestCheckResourceAttr(fqrn, "audiences.#", "2"), resource.TestCheckTypeSetElemAttr(fqrn, "audiences.*", "jfrt@1"), @@ -321,7 +379,7 @@ func TestAccScopedToken_WithGroupScope(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: accessTokenConfig, @@ -355,7 +413,7 @@ func TestAccScopedToken_WithInvalidScopes(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: scopedTokenConfig, @@ -463,7 +521,7 @@ func mkAudienceTestCase(prefix string, t *testing.T) (*testing.T, resource.TestC return t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: accessTokenConfig, @@ -496,7 +554,7 @@ func TestAccScopedToken_WithInvalidAudiences(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: scopedTokenConfig, @@ -529,7 +587,7 @@ func TestAccScopedToken_WithTooLongAudiences(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: scopedTokenConfig, @@ -566,7 +624,7 @@ func TestAccScopedToken_WithExpiresInLessThanPersistencyThreshold(t *testing.T) resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: accessTokenConfig, @@ -602,7 +660,7 @@ func TestAccScopedToken_WithExpiresInSetToZeroForNonExpiringToken(t *testing.T) resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: accessTokenConfig, diff --git a/pkg/artifactory/resource/user/resource_artifactory_anonymous_user_test.go b/pkg/artifactory/resource/user/resource_artifactory_anonymous_user_test.go index efc25978..c00b83b3 100644 --- a/pkg/artifactory/resource/user/resource_artifactory_anonymous_user_test.go +++ b/pkg/artifactory/resource/user/resource_artifactory_anonymous_user_test.go @@ -19,7 +19,7 @@ func TestAccAnonymousUser_Importable(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: anonymousUserConfig, @@ -45,7 +45,7 @@ func TestAccAnonymousUser_NotCreatable(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { Config: anonymousUserConfig, diff --git a/pkg/artifactory/resource/user/resource_artifactory_managed_user_test.go b/pkg/artifactory/resource/user/resource_artifactory_managed_user_test.go index 32442d7d..7b855503 100644 --- a/pkg/artifactory/resource/user/resource_artifactory_managed_user_test.go +++ b/pkg/artifactory/resource/user/resource_artifactory_managed_user_test.go @@ -161,7 +161,7 @@ func testAccManagedUserInvalidName(t *testing.T, username, errorRegex string) fu resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckUserDestroy(fqrn), Steps: []resource.TestStep{ { diff --git a/pkg/artifactory/resource/user/resource_artifactory_user_test.go b/pkg/artifactory/resource/user/resource_artifactory_user_test.go index 722927fc..4425c5d0 100644 --- a/pkg/artifactory/resource/user/resource_artifactory_user_test.go +++ b/pkg/artifactory/resource/user/resource_artifactory_user_test.go @@ -82,7 +82,7 @@ func TestAccUser_basic_groups(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: testAccCheckManagedUserDestroy(fqrn), Steps: []resource.TestStep{ @@ -124,7 +124,7 @@ func TestAccUser_no_password(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: testAccCheckManagedUserDestroy(fqrn), Steps: []resource.TestStep{ @@ -166,7 +166,7 @@ func TestAccUser_no_groups(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: testAccCheckManagedUserDestroy(fqrn), Steps: []resource.TestStep{ @@ -209,7 +209,7 @@ func TestAccUser_empty_groups(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, PreCheck: func() { acctest.PreCheck(t) }, CheckDestroy: testAccCheckManagedUserDestroy(fqrn), Steps: []resource.TestStep{ @@ -259,7 +259,7 @@ func testAccUserInvalidName(t *testing.T, username, errorRegex string) func(t *t resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckUserDestroy(fqrn), Steps: []resource.TestStep{ { @@ -300,7 +300,7 @@ func TestAccUser_all_attributes(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckUserDestroy(fqrn), Steps: []resource.TestStep{ { @@ -372,7 +372,7 @@ func TestAccUser_PasswordNotChangeWhenOtherAttributesChangeGH340(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5MuxProviderFactories, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckUserDestroy(fqrn), Steps: []resource.TestStep{ { diff --git a/templates/resources/mail_server.md.tmpl b/templates/resources/mail_server.md.tmpl new file mode 100644 index 00000000..3b2e328c --- /dev/null +++ b/templates/resources/mail_server.md.tmpl @@ -0,0 +1,29 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{ .Name }} Resource - {{ .ProviderName }}" +subcategory: "Configuration" +--- + +# Artifactory Mail Server Resource + +Provides an Artifactory Mail Server resource. This can be used to create and manage Artifactory mail server configuration. + +## Example Usages + +{{tffile (printf "examples/resources/%s/resource.tf" .Name) }} + +## Argument reference + +{{ .SchemaMarkdown | trimspace }} + +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" .ImportFile }} + +~>The `password` attribute is not retrievable from Artifactory thus there will be state drift after importing this resource. + +{{- end }}