diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dc3771f7..ad9691739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 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: @@ -10,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/docs/resources/mail_server.md b/docs/resources/mail_server.md new file mode 100644 index 000000000..1d1b0ee0f --- /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/examples/resources/artifactory_mail_server/import.sh b/examples/resources/artifactory_mail_server/import.sh new file mode 100644 index 000000000..46fe04361 --- /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 000000000..4389f6ad7 --- /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 88e07c70d..b49b32596 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 5c825af4f..dbaa09d91 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 d1a6821dc..f32aea08c 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 000000000..b6841db52 --- /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 5428e9588..a651c202c 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 1f9574452..e76e3e2bc 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 36f4bafa7..5d744a0aa 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 000000000..92f2f87f8 --- /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 000000000..7d7923335 --- /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 20690de1a..5b2d76f86 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 82094346a..8e79452f0 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_test.go b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go index 636254637..ac9a067e6 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go +++ b/pkg/artifactory/resource/security/resource_artifactory_scoped_token_test.go @@ -246,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{ { @@ -318,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) @@ -379,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, @@ -413,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, @@ -521,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, @@ -554,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, @@ -587,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, @@ -624,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, @@ -660,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 efc25978a..c00b83b36 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 32442d7d3..7b8555035 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 722927fc6..4425c5d03 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 000000000..3b2e328cf --- /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 }}