Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New resource: azurerm_mssql_job_target_group #28492

Merged
merged 12 commits into from
Jan 30, 2025
2 changes: 1 addition & 1 deletion .github/labeler-issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ service/monitor:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_monitor_((.|\n)*)###'

service/mssql:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(mssql_database\W+|mssql_database_extended_auditing_policy\W+|mssql_database_vulnerability_assessment_rule_baseline\W+|mssql_elasticpool\W+|mssql_failover_group\W+|mssql_firewall_rule\W+|mssql_job\W+|mssql_job_agent\W+|mssql_job_credential\W+|mssql_job_schedule\W+|mssql_outbound_firewall_rule\W+|mssql_server\W+|mssql_server_dns_alias\W+|mssql_server_extended_auditing_policy\W+|mssql_server_microsoft_support_auditing_policy\W+|mssql_server_security_alert_policy\W+|mssql_server_transparent_data_encryption\W+|mssql_server_vulnerability_assessment\W+|mssql_virtual_machine\W+|mssql_virtual_machine_availability_group_listener\W+|mssql_virtual_machine_group\W+|mssql_virtual_network_rule\W+)((.|\n)*)###'
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(mssql_database\W+|mssql_database_extended_auditing_policy\W+|mssql_database_vulnerability_assessment_rule_baseline\W+|mssql_elasticpool\W+|mssql_failover_group\W+|mssql_firewall_rule\W+|mssql_job\W+|mssql_job_agent\W+|mssql_job_credential\W+|mssql_job_schedule\W+|mssql_job_target_group\W+|mssql_outbound_firewall_rule\W+|mssql_server\W+|mssql_server_dns_alias\W+|mssql_server_extended_auditing_policy\W+|mssql_server_microsoft_support_auditing_policy\W+|mssql_server_security_alert_policy\W+|mssql_server_transparent_data_encryption\W+|mssql_server_vulnerability_assessment\W+|mssql_virtual_machine\W+|mssql_virtual_machine_availability_group_listener\W+|mssql_virtual_machine_group\W+|mssql_virtual_network_rule\W+)((.|\n)*)###'

service/mssqlmanagedinstance:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_mssql_managed_((.|\n)*)###'
Expand Down
9 changes: 9 additions & 0 deletions internal/services/mssql/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/jobagents"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/jobcredentials"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/jobs"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/jobtargetgroups"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/longtermretentionpolicies"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/outboundfirewallrules"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/replicationlinks"
Expand Down Expand Up @@ -55,6 +56,7 @@ type Client struct {
JobAgentsClient *jobagents.JobAgentsClient
JobCredentialsClient *jobcredentials.JobCredentialsClient
JobsClient *jobs.JobsClient
JobTargetGroupsClient *jobtargetgroups.JobTargetGroupsClient
LongTermRetentionPoliciesClient *longtermretentionpolicies.LongTermRetentionPoliciesClient
OutboundFirewallRulesClient *outboundfirewallrules.OutboundFirewallRulesClient
ReplicationLinksClient *replicationlinks.ReplicationLinksClient
Expand Down Expand Up @@ -156,6 +158,12 @@ func NewClient(o *common.ClientOptions) (*Client, error) {
}
o.Configure(jobsClient.Client, o.Authorizers.ResourceManager)

jobTargetGroupsClient, err := jobtargetgroups.NewJobTargetGroupsClientWithBaseURI(o.Environment.ResourceManager)
if err != nil {
return nil, fmt.Errorf("building Job Target Groups Client: %+v", err)
}
o.Configure(jobTargetGroupsClient.Client, o.Authorizers.ResourceManager)

longTermRetentionPoliciesClient, err := longtermretentionpolicies.NewLongTermRetentionPoliciesClientWithBaseURI(o.Environment.ResourceManager)
if err != nil {
return nil, fmt.Errorf("building Long Term Retention Policies Client: %+v", err)
Expand Down Expand Up @@ -301,6 +309,7 @@ func NewClient(o *common.ClientOptions) (*Client, error) {
ElasticPoolsClient: elasticPoolsClient,
GeoBackupPoliciesClient: geoBackupPoliciesClient,
JobsClient: jobsClient,
JobTargetGroupsClient: jobTargetGroupsClient,
LongTermRetentionPoliciesClient: longTermRetentionPoliciesClient,
ReplicationLinksClient: replicationLinksClient,
RestorableDroppedDatabasesClient: restorableDroppedDatabasesClient,
Expand Down
351 changes: 351 additions & 0 deletions internal/services/mssql/mssql_job_target_group_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
package mssql

import (
"context"
"fmt"
"time"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/jobcredentials"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/jobs"
"github.com/hashicorp/go-azure-sdk/resource-manager/sql/2023-08-01-preview/jobtargetgroups"
"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/mssql/validate"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation"
)

type MsSqlJobTargetGroupResource struct{}

type MsSqlJobTargetGroupResourceModel struct {
Name string `tfschema:"name"`
JobAgentID string `tfschema:"job_agent_id"`
JobTargets []MsSqlJobTarget `tfschema:"job_target"`
}

type MsSqlJobTarget struct {
ServerName string `tfschema:"server_name"`
Type string `tfschema:"type"`
DatabaseName string `tfschema:"database_name"`
ElasticPoolName string `tfschema:"elastic_pool_name"`
JobCredentialId string `tfschema:"job_credential_id"`
MembershipType string `tfschema:"membership_type"`
}

var (
_ sdk.ResourceWithUpdate = MsSqlJobTargetGroupResource{}
_ sdk.ResourceWithCustomizeDiff = MsSqlJobTargetGroupResource{}
)

func (r MsSqlJobTargetGroupResource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
ForceNew: true,
},
"job_agent_id": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: jobs.ValidateJobAgentID,
ForceNew: true,
},
"job_target": {
Type: pluginsdk.TypeSet,
Optional: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"server_name": {
sreallymatt marked this conversation as resolved.
Show resolved Hide resolved
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validate.ValidateMsSqlServerName,
},
"database_name": {
Type: pluginsdk.TypeString,
Optional: true,
ValidateFunc: validate.ValidateMsSqlDatabaseName,
},
"elastic_pool_name": {
Type: pluginsdk.TypeString,
Optional: true,
ValidateFunc: validate.ValidateMsSqlElasticPoolName,
},
sreallymatt marked this conversation as resolved.
Show resolved Hide resolved
"job_credential_id": {
Type: pluginsdk.TypeString,
Optional: true,
ValidateFunc: jobcredentials.ValidateCredentialID,
},
"membership_type": {
Type: pluginsdk.TypeString,
Optional: true,
Default: string(jobtargetgroups.JobTargetGroupMembershipTypeInclude),
ValidateFunc: validation.StringInSlice(jobtargetgroups.PossibleValuesForJobTargetGroupMembershipType(), false),
},
"type": {
Type: pluginsdk.TypeString,
Computed: true,
},
},
},
},
}
}

func (r MsSqlJobTargetGroupResource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{}
}

func (r MsSqlJobTargetGroupResource) CustomizeDiff() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 10 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
var config MsSqlJobTargetGroupResourceModel
if err := metadata.DecodeDiff(&config); err != nil {
return fmt.Errorf("decoding: %+v", err)
}

for _, v := range config.JobTargets {
if v.DatabaseName != "" && v.ElasticPoolName != "" {
return fmt.Errorf("`database_name` and `elastic_pool_name` are mutually exclusive")
}

targetType := determineJobTargetType(v)
if isCredentialRequired(jobtargetgroups.JobTargetGroupMembershipType(v.MembershipType), targetType) {
if v.JobCredentialId == "" {
return fmt.Errorf("`job_credential_id` is required when `membership_type` is `%s` and `type` is `%s`", jobtargetgroups.JobTargetGroupMembershipTypeInclude, targetType)
}
}
}

return nil
},
}
}

func (r MsSqlJobTargetGroupResource) ModelObject() interface{} {
return &MsSqlJobTargetGroupResourceModel{}
}

func (r MsSqlJobTargetGroupResource) ResourceType() string {
return "azurerm_mssql_job_target_group"
}

func (r MsSqlJobTargetGroupResource) Create() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.MSSQL.JobTargetGroupsClient

var model MsSqlJobTargetGroupResourceModel
if err := metadata.Decode(&model); err != nil {
return err
}

jobAgent, err := jobtargetgroups.ParseJobAgentID(model.JobAgentID)
if err != nil {
return err
}

id := jobtargetgroups.NewTargetGroupID(jobAgent.SubscriptionId, jobAgent.ResourceGroupName, jobAgent.ServerName, jobAgent.JobAgentName, model.Name)

existing, err := client.Get(ctx, id)
if err != nil && !response.WasNotFound(existing.HttpResponse) {
return fmt.Errorf("checking for presence of existing %s: %+v", id, err)
}

if !response.WasNotFound(existing.HttpResponse) {
return metadata.ResourceRequiresImport(r.ResourceType(), id)
}

parameters := jobtargetgroups.JobTargetGroup{
Name: pointer.To(model.Name),
Properties: pointer.To(jobtargetgroups.JobTargetGroupProperties{
Members: expandJobTargets(model.JobTargets),
}),
}

if _, err := client.CreateOrUpdate(ctx, id, parameters); err != nil {
return fmt.Errorf("creating %s: %+v", id, err)
}

metadata.SetID(id)
return nil
},
}
}

func (r MsSqlJobTargetGroupResource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.MSSQL.JobTargetGroupsClient

id, err := jobtargetgroups.ParseTargetGroupID(metadata.ResourceData.Id())
if err != nil {
return err
}

resp, err := client.Get(ctx, *id)
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return metadata.MarkAsGone(id)
}

return fmt.Errorf("retrieving %s: %+v", id, err)
}

state := MsSqlJobTargetGroupResourceModel{
Name: id.TargetGroupName,
JobAgentID: jobtargetgroups.NewJobAgentID(id.SubscriptionId, id.ResourceGroupName, id.ServerName, id.JobAgentName).ID(),
}

if model := resp.Model; model != nil {
if props := model.Properties; props != nil {
state.JobTargets = flattenJobTargets(props.Members)
}
}

return metadata.Encode(&state)
},
}
}

func (r MsSqlJobTargetGroupResource) Update() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.MSSQL.JobTargetGroupsClient

id, err := jobtargetgroups.ParseTargetGroupID(metadata.ResourceData.Id())
if err != nil {
return err
}

var config MsSqlJobTargetGroupResourceModel
if err := metadata.Decode(&config); err != nil {
return fmt.Errorf("decoding %+v", err)
}

existing, err := client.Get(ctx, *id)
if err != nil {
return fmt.Errorf("retrieving %s: %+v", id, err)
}

if existing.Model == nil {
return fmt.Errorf("retrieving %s: `model` was nil", id)
}

if existing.Model.Properties == nil {
return fmt.Errorf("retrieving %s: `model.Properties` was nil", id)
}

payload := existing.Model

if metadata.ResourceData.HasChange("job_target") {
payload.Properties.Members = expandJobTargets(config.JobTargets)
}

if _, err := client.CreateOrUpdate(ctx, *id, *payload); err != nil {
return fmt.Errorf("updating: %s: %+v", id, err)
}

return nil
},
}
}

func (r MsSqlJobTargetGroupResource) Delete() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.MSSQL.JobTargetGroupsClient

id, err := jobtargetgroups.ParseTargetGroupID(metadata.ResourceData.Id())
if err != nil {
return err
}

if _, err := client.Delete(ctx, *id); err != nil {
return fmt.Errorf("deleting %s: %+v", id, err)
}

return nil
},
}
}

func (r MsSqlJobTargetGroupResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
return jobtargetgroups.ValidateTargetGroupID
}

func expandJobTargets(input []MsSqlJobTarget) []jobtargetgroups.JobTarget {
targets := make([]jobtargetgroups.JobTarget, 0)
if len(input) == 0 {
return targets
}

for _, v := range input {
t := jobtargetgroups.JobTarget{
MembershipType: pointer.To(jobtargetgroups.JobTargetGroupMembershipType(v.MembershipType)),
ServerName: pointer.To(v.ServerName),
}

targetType := determineJobTargetType(v)
t.Type = targetType

if isCredentialRequired(jobtargetgroups.JobTargetGroupMembershipType(v.MembershipType), targetType) {
t.RefreshCredential = pointer.To(v.JobCredentialId)
}

if targetType == jobtargetgroups.JobTargetTypeSqlDatabase {
t.DatabaseName = pointer.To(v.DatabaseName)
}

if targetType == jobtargetgroups.JobTargetTypeSqlElasticPool {
t.ElasticPoolName = pointer.To(v.ElasticPoolName)
}

targets = append(targets, t)
}

return targets
}

func flattenJobTargets(input []jobtargetgroups.JobTarget) []MsSqlJobTarget {
targets := make([]MsSqlJobTarget, 0)
if len(input) == 0 {
return targets
}

for _, v := range input {
t := MsSqlJobTarget{
DatabaseName: pointer.From(v.DatabaseName),
ElasticPoolName: pointer.From(v.ElasticPoolName),
MembershipType: string(pointer.From(v.MembershipType)),
JobCredentialId: pointer.From(v.RefreshCredential),
ServerName: pointer.From(v.ServerName),
Type: string(v.Type),
}

targets = append(targets, t)
}

return targets
}

func determineJobTargetType(input MsSqlJobTarget) jobtargetgroups.JobTargetType {
switch {
case input.DatabaseName != "":
return jobtargetgroups.JobTargetTypeSqlDatabase
case input.ElasticPoolName != "":
return jobtargetgroups.JobTargetTypeSqlElasticPool
default:
return jobtargetgroups.JobTargetTypeSqlServer
}
}

func isCredentialRequired(membershipType jobtargetgroups.JobTargetGroupMembershipType, targetType jobtargetgroups.JobTargetType) bool {
return membershipType == jobtargetgroups.JobTargetGroupMembershipTypeInclude && targetType != jobtargetgroups.JobTargetTypeSqlDatabase
}
Loading
Loading