Compare commits

..
4 Commits
Author SHA1 Message Date
Hugo 88798fb76b fix(plugin): Panic in tests if err variable is nil 2024-07-15 16:27:00 +01:00
Hugo 5626a997c8 fix(plugin): Remove AWS region validation
The check in `PartitionForRegion` was a static check against a list of
known regions. This would cause an error if the SDK was not up-to-date.
2024-07-15 16:27:00 +01:00
Michael Li 6cfccab32a chore: Update to use Node20 version of Actions (#43) 2024-05-03 15:50:39 -04:00
Hugo 282935aa79 chore(README): Fix Endpoint URL attribute 2024-02-07 23:18:49 +00:00
8 changed files with 9 additions and 256 deletions

No files matched your search

+2 -2
View File
@@ -13,9 +13,9 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Set up Go
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version-file: 'go.mod'
cache: true
-1
View File
@@ -3,7 +3,6 @@ module github.com/hashicorp/boundary-plugin-aws
go 1.21
require (
github.com/aws/aws-sdk-go v1.44.80
github.com/aws/aws-sdk-go-v2 v1.20.1
github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0
github.com/aws/aws-sdk-go-v2/service/iam v1.22.2
-8
View File
@@ -8,8 +8,6 @@ github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.44.80 h1:jEXGecSgPdvM5KnyDsSgFhZSm7WwaTp4h544Im4SfhI=
github.com/aws/aws-sdk-go v1.44.80/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.20.1 h1:rZBf5DWr7YGrnlTK4kgDQGn1ltqOg5orCYb/UhOFZkg=
github.com/aws/aws-sdk-go-v2 v1.20.1/go.mod h1:NU06lETsFm8fUC6ZjhgDpVBcGZTFQ6XM+LZWZxMI4ac=
@@ -177,7 +175,6 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
@@ -226,7 +223,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -237,21 +233,17 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-7
View File
@@ -6,7 +6,6 @@ package credential
import (
"fmt"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
@@ -132,12 +131,6 @@ func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error)
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = err.Error()
}
if region != "" {
if _, found := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region); !found {
badFields[fmt.Sprintf("attributes.%s", ConstRegion)] = fmt.Sprintf("not a valid region: %s", region)
}
}
disableCredentialRotation, err := values.GetBoolValue(in, ConstDisableCredentialRotation, false)
if err != nil {
badFields[fmt.Sprintf("attributes.%s", ConstDisableCredentialRotation)] = err.Error()
-7
View File
@@ -25,13 +25,6 @@ func TestGetCredentialAttributes(t *testing.T) {
in: map[string]any{},
expectedErrContains: "missing required value \"region\"",
},
{
name: "invalid aws region value",
in: map[string]any{
ConstRegion: "dne",
},
expectedErrContains: "not a valid region",
},
{
name: "bad value for disable_credential_rotation",
in: map[string]any{
+6 -87
View File
@@ -79,27 +79,6 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
expectedErrContains: "missing required value \"access_key_id\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
}},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.OnCreateCatalogRequest{
@@ -198,6 +177,7 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
testCatalogStateOpts: tc.catalogOpts,
}
_, err := p.OnCreateCatalog(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
})
@@ -242,22 +222,6 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.OnUpdateCatalogRequest{
@@ -425,6 +389,7 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
testCatalogStateOpts: tc.catalogOpts,
}
_, err := p.OnUpdateCatalog(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
})
@@ -515,6 +480,7 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
testCatalogStateOpts: tc.catalogOpts,
}
_, err := p.OnDeleteCatalog(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
})
@@ -559,22 +525,6 @@ func TestPluginOnCreateSetErr(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.OnCreateSetRequest{
@@ -883,6 +833,7 @@ func TestPluginOnCreateSetErr(t *testing.T) {
testCatalogStateOpts: tc.catalogOpts,
}
_, err := p.OnCreateSet(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
})
@@ -927,23 +878,6 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.OnUpdateSetRequest{
@@ -1207,6 +1141,7 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
testCatalogStateOpts: tc.catalogOpts,
}
_, err := p.OnUpdateSet(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
})
@@ -1251,23 +1186,6 @@ func TestPluginListHostsErr(t *testing.T) {
expectedErrContains: "attributes.region: missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
},
expectedErrContains: "not a valid region: ",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.ListHostsRequest{
@@ -1586,6 +1504,7 @@ func TestPluginListHostsErr(t *testing.T) {
testCatalogStateOpts: tc.catalogOpts,
}
_, err := p.ListHosts(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
})
+1 -1
View File
@@ -68,7 +68,7 @@ The following `attributes` are valid on an AWS storage bucket resource:
- `disable_credential_rotation` (bool): If `true`, credential rotation will not
be performed.
- `region` (string): The region to configure the storage bucket for.
- `endpoint-url` (string): The endpoint to configure the storage.
- `endpoint_url` (string): The endpoint to configure the storage.
- `role_arn` (string): The role arn configured for the assume role provider.
- `role_external_id` (string): The external id configured for the assume role provider.
- `role_session_name` (string): The session name configured for the assume role provider.
-143
View File
@@ -92,21 +92,6 @@ func TestStoragePlugin_OnCreateStorageBucket(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnCreateStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "dynamic credentials without disable credential rotation",
req: &pb.OnCreateStorageBucketRequest{
@@ -536,28 +521,6 @@ func TestStoragePlugin_OnUpdateStorageBucket(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnUpdateStorageBucketRequest{
NewBucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
CurrentBucket: &storagebuckets.StorageBucket{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.OnUpdateStorageBucketRequest{
@@ -1603,22 +1566,6 @@ func TestStoragePlugin_OnDeleteStorageBucket(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.OnDeleteStorageBucketRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: credential.MockStaticCredentialSecrets(),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "persisted state setup error",
req: &pb.OnDeleteStorageBucketRequest{
@@ -1822,23 +1769,6 @@ func TestStoragePlugin_HeadObject(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.HeadObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
req: validRequest(),
@@ -2025,22 +1955,6 @@ func TestStoragePlugin_ValidatePermissions(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.ValidatePermissionsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
req: validRequest(),
@@ -2310,23 +2224,6 @@ func TestStoragePlugin_GetObject(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.GetObjectRequest{
Key: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
req: validRequest(),
@@ -2669,29 +2566,6 @@ func TestStoragePlugin_PutObject(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
request: &pb.PutObjectRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "external-obj-store",
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
Key: "mock-object",
Path: validFilePath,
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
request: validRequest(),
@@ -2970,23 +2844,6 @@ func TestStoragePlugin_DeleteObjects(t *testing.T) {
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "invalid region",
req: &pb.DeleteObjectsRequest{
KeyPrefix: "/foo/bar/key",
Bucket: &storagebuckets.StorageBucket{
BucketName: "foo",
Secrets: new(structpb.Struct),
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("foobar"),
},
},
},
},
expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
},
{
name: "credential persisted state setup error",
req: validRequest(),