Skip to content

Commit

Permalink
Merge pull request #38307 from myerscf/feature/aws_service_principal_…
Browse files Browse the repository at this point in the history
…name_data_source

feat(spn): created service principal name data source
  • Loading branch information
YakDriver authored Jul 24, 2024
2 parents fe0ab1d + 15f2d71 commit f90bb3b
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .changelog/38307.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-data-source
aws_service_principal
```
2 changes: 1 addition & 1 deletion .github/labeler-issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ service/mediatailor:
service/memorydb:
- '((\*|-)\s*`?|(data|resource)\s+"?)aws_memorydb_'
service/meta:
- '((\*|-)\s*`?|(data|resource)\s+"?)aws_(arn|billing_service_account|default_tags|ip_ranges|partition|regions?|service)$'
- '((\*|-)\s*`?|(data|resource)\s+"?)aws_(arn|billing_service_account|default_tags|ip_ranges|partition|regions?|service|service_principal)$'
service/mgh:
- '((\*|-)\s*`?|(data|resource)\s+"?)aws_mgh_'
service/mgn:
Expand Down
1 change: 1 addition & 0 deletions .github/labeler-pr-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1460,6 +1460,7 @@ service/meta:
- 'website/**/partition*'
- 'website/**/region*'
- 'website/**/service\.*'
- 'website/**/service_principal*'
service/mgh:
- any:
- changed-files:
Expand Down
3 changes: 3 additions & 0 deletions internal/service/meta/service_package_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 113 additions & 0 deletions internal/service/meta/service_principal_data_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package meta

import (
"context"

"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-aws/internal/framework"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @FrameworkDataSource
func newServicePrincipalDataSource(context.Context) (datasource.DataSourceWithConfigure, error) {
d := &servicePrincipalDataSource{}

return d, nil
}

type servicePrincipalDataSource struct {
framework.DataSourceWithConfigure
}

func (*servicePrincipalDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { // nosemgrep:ci.meta-in-func-name
response.TypeName = "aws_service_principal"
}

func (d *servicePrincipalDataSource) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) {
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
names.AttrID: schema.StringAttribute{
Computed: true,
},
names.AttrName: schema.StringAttribute{
Computed: true,
},
names.AttrRegion: schema.StringAttribute{
Optional: true,
Computed: true,
},
names.AttrServiceName: schema.StringAttribute{
Required: true,
},
"suffix": schema.StringAttribute{
Computed: true,
},
},
}
}

func (d *servicePrincipalDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) {
var data servicePrincipalDataSourceModel
response.Diagnostics.Append(request.Config.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

var region *endpoints.Region

// find the region given by the user
if !data.Region.IsNull() {
matchingRegion, err := FindRegionByName(data.Region.ValueString())

if err != nil {
response.Diagnostics.AddError("finding Region by name", err.Error())

return
}

region = matchingRegion
}

// Default to provider current region if no other filters matched
if region == nil {
matchingRegion, err := FindRegionByName(d.Meta().Region)

if err != nil {
response.Diagnostics.AddError("finding Region using the provider", err.Error())

return
}

region = matchingRegion
}

partition := names.PartitionForRegion(region.ID())

serviceName := ""

if !data.ServiceName.IsNull() {
serviceName = data.ServiceName.ValueString()
}

SourceServicePrincipal := names.ServicePrincipalNameForPartition(serviceName, partition)

data.ID = types.StringValue(serviceName + "." + region.ID() + "." + SourceServicePrincipal)
data.Name = types.StringValue(serviceName + "." + SourceServicePrincipal)
data.Suffix = types.StringValue(SourceServicePrincipal)
data.Region = types.StringValue(region.ID())
response.Diagnostics.Append(response.State.Set(ctx, &data)...)
}

type servicePrincipalDataSourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Region types.String `tfsdk:"region"`
ServiceName types.String `tfsdk:"service_name"`
Suffix types.String `tfsdk:"suffix"`
}
173 changes: 173 additions & 0 deletions internal/service/meta/service_principal_data_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package meta_test

import (
"fmt"
"testing"

"github.com/YakDriver/regexache"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
tfmeta "github.com/hashicorp/terraform-provider-aws/internal/service/meta"
"github.com/hashicorp/terraform-provider-aws/names"
)

func TestAccMetaServicePrincipal_basic(t *testing.T) {
ctx := acctest.Context(t)
dataSourceName := "data.aws_service_principal.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, tfmeta.PseudoServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccSPNDataSourceConfig_basic,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, names.AttrID, "s3."+acctest.Region()+".amazonaws.com"),
resource.TestCheckResourceAttr(dataSourceName, names.AttrName, "s3.amazonaws.com"),
resource.TestCheckResourceAttr(dataSourceName, "suffix", "amazonaws.com"),
resource.TestCheckResourceAttr(dataSourceName, names.AttrRegion, acctest.Region()),
resource.TestCheckResourceAttr(dataSourceName, names.AttrServiceName, "s3"),
),
},
},
})
}

func TestAccMetaServicePrincipal_MissingService(t *testing.T) {
ctx := acctest.Context(t)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, tfmeta.PseudoServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccSPNDataSourceConfig_empty,
ExpectError: regexache.MustCompile(`The argument "service_name" is required, but no definition was found.`),
},
},
})
}

func TestAccMetaServicePrincipal_ByRegion(t *testing.T) {
ctx := acctest.Context(t)

dataSourceName := "data.aws_service_principal.test"
regions := []string{"us-east-1", "cn-north-1", "us-gov-east-1", "us-iso-east-1", "us-isob-east-1", "eu-isoe-west-1"} //lintignore:AWSAT003

for _, region := range regions {
t.Run(region, func(t *testing.T) {
t.Parallel()
resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, tfmeta.PseudoServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccSPNDataSourceConfig_withRegion("s3", region),
Check: resource.ComposeTestCheckFunc(
//lintignore:AWSR001
resource.TestCheckResourceAttr(dataSourceName, names.AttrID, fmt.Sprintf("s3.%s.amazonaws.com", region)),
resource.TestCheckResourceAttr(dataSourceName, names.AttrName, "s3.amazonaws.com"),
resource.TestCheckResourceAttr(dataSourceName, "suffix", "amazonaws.com"),
resource.TestCheckResourceAttr(dataSourceName, names.AttrRegion, region),
),
},
},
})
})
}
}

func TestAccMetaServicePrincipal_UniqueForServiceInRegion(t *testing.T) {
ctx := acctest.Context(t)
dataSourceName := "data.aws_service_principal.test"

type spnTestCase struct {
Service string
Region string
Suffix string
ID string
SPN string
}

var testCases []spnTestCase

var regionUniqueServices = []struct {
Region string
Suffix string
Services []string
}{
{
Region: "us-iso-east-1", //lintignore:AWSAT003
Suffix: "c2s.ic.gov",
Services: []string{"cloudhsm", "config", "logs", "workspaces"},
},
{
Region: "us-isob-east-1", //lintignore:AWSAT003
Suffix: "sc2s.sgov.gov",
Services: []string{"dms", "logs"},
},
{
Region: "cn-north-1", //lintignore:AWSAT003
Suffix: "amazonaws.com.cn",
Services: []string{"codedeploy", "elasticmapreduce", "logs"},
},
}

for _, region := range regionUniqueServices {
for _, service := range region.Services {
testCases = append(testCases, spnTestCase{
Service: service,
Region: region.Region,
Suffix: region.Suffix,
ID: fmt.Sprintf("%s.%s.%s", service, region.Region, region.Suffix),
SPN: fmt.Sprintf("%s.%s", service, region.Suffix),
})
}
}

for _, testCase := range testCases {
t.Run(fmt.Sprintf("%s/%s", testCase.Region, testCase.Service), func(t *testing.T) {
t.Parallel()
resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, tfmeta.PseudoServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccSPNDataSourceConfig_withRegion(testCase.Service, testCase.Region),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, names.AttrID, testCase.ID),
resource.TestCheckResourceAttr(dataSourceName, names.AttrName, testCase.SPN),
resource.TestCheckResourceAttr(dataSourceName, "suffix", testCase.Suffix),
resource.TestCheckResourceAttr(dataSourceName, names.AttrRegion, testCase.Region),
),
},
},
})
})
}
}

const testAccSPNDataSourceConfig_empty = `
data "aws_service_principal" "test" {}
`
const testAccSPNDataSourceConfig_basic = `
data "aws_service_principal" "test" {
service_name = "s3"
}
`

func testAccSPNDataSourceConfig_withRegion(service string, region string) string {
return fmt.Sprintf(`
data "aws_service_principal" "test" {
region = %[1]q
service_name = %[2]q
}
`, region, service)
}
4 changes: 2 additions & 2 deletions names/data/names_data.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -6130,12 +6130,12 @@ service "meta" {
}

resource_prefix {
actual = "aws_(arn|billing_service_account|default_tags|ip_ranges|partition|regions?|service)$"
actual = "aws_(arn|billing_service_account|default_tags|ip_ranges|partition|regions?|service|service_principal)$"
correct = "aws_meta_"
}

provider_package_correct = "meta"
doc_prefix = ["arn", "ip_ranges", "billing_service_account", "default_tags", "partition", "region", "service\\."]
doc_prefix = ["arn", "ip_ranges", "billing_service_account", "default_tags", "partition", "region", "service\\.", "service_principal"]
brand = ""
exclude = true
allowed_subcategory = true
Expand Down
45 changes: 45 additions & 0 deletions names/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,51 @@ func DNSSuffixForPartition(partition string) string {
}
}

func ServicePrincipalSuffixForPartition(partition string) string {
switch partition {
case ChinaPartitionID:
return "amazonaws.com.cn"
case ISOPartitionID:
return "c2s.ic.gov"
case ISOBPartitionID:
return "sc2s.sgov.gov"
default:
return "amazonaws.com"
}
}

// SPN region unique taken from
// https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/region-info/lib/default.ts
func ServicePrincipalNameForPartition(service string, partition string) string {
if service != "" && partition != StandardPartitionID {
switch partition {
case ISOPartitionID:
switch service {
case "cloudhsm",
"config",
"logs",
"workspaces":
return DNSSuffixForPartition(partition)
}
case ISOBPartitionID:
switch service {
case "dms",
"logs":
return DNSSuffixForPartition(partition)
}
case ChinaPartitionID:
switch service {
case "codedeploy",
"elasticmapreduce",
"logs":
return DNSSuffixForPartition(partition)
}
}
}

return "amazonaws.com"
}

func IsOptInRegion(region string) bool {
switch region {
case AFSouth1RegionID,
Expand Down
Loading

0 comments on commit f90bb3b

Please sign in to comment.