diff --git a/internal/config/config.go b/internal/config/config.go index 2d0a727..77c7a78 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "path/filepath" + "strconv" "strings" "gopkg.in/yaml.v3" @@ -150,6 +151,12 @@ func expandTiers(value string) map[string]bool { } func applyEnvOverrides(cfg *Config) { + if p := os.Getenv("DEVCLOUD_PORT"); p != "" { + if v, err := strconv.Atoi(p); err == nil { + cfg.Server.Port = v + } + } + if envServices := os.Getenv("DEVCLOUD_SERVICES"); envServices != "" { allowed := expandTiers(envServices) if allowed == nil { diff --git a/internal/services/bedrock/provider.go b/internal/services/bedrock/provider.go index d40fc62..143463f 100644 --- a/internal/services/bedrock/provider.go +++ b/internal/services/bedrock/provider.go @@ -504,7 +504,11 @@ func (p *Provider) listTagsForResource(arn string) (*plugin.Response, error) { if err != nil { return shared.JSONError("InternalError", err.Error(), http.StatusInternalServerError), nil } - return shared.JSONResponse(http.StatusOK, map[string]any{"tags": tags}) + tagList := make([]map[string]string, 0, len(tags)) + for k, v := range tags { + tagList = append(tagList, map[string]string{"key": k, "value": v}) + } + return shared.JSONResponse(http.StatusOK, map[string]any{"tags": tagList}) } func (p *Provider) untagResource(arn string, keys []string) (*plugin.Response, error) { diff --git a/internal/services/bedrock/provider_test.go b/internal/services/bedrock/provider_test.go index 4d4b8f9..71180f2 100644 --- a/internal/services/bedrock/provider_test.go +++ b/internal/services/bedrock/provider_test.go @@ -184,10 +184,22 @@ func TestTagging(t *testing.T) { resp2 := call(t, p, "GET", "/tags/"+testARN, "") assert.Equal(t, 200, resp2.StatusCode) rb2 := parseBody(t, resp2) - tags := rb2["tags"].(map[string]any) - assert.Equal(t, "test", tags["env"]) - - // Untag - resp3 := call(t, p, "DELETE", "/tags/"+testARN+"?tagKeys=env", "") + tagList := rb2["tags"].([]any) + tagMap := map[string]string{} + for _, t := range tagList { + tm := t.(map[string]any) + tagMap[tm["key"].(string)] = tm["value"].(string) + } + assert.Equal(t, "test", tagMap["env"]) + + // Untag all + resp3 := call(t, p, "DELETE", "/tags/"+testARN+"?tagKeys=env&tagKeys=tier", "") assert.Equal(t, 204, resp3.StatusCode) + + // List after removing all tags + resp4 := call(t, p, "GET", "/tags/"+testARN, "") + assert.Equal(t, 200, resp4.StatusCode) + rb4 := parseBody(t, resp4) + emptyTags := rb4["tags"].([]any) + assert.Empty(t, emptyTags) } diff --git a/internal/services/cloudwatch/provider.go b/internal/services/cloudwatch/provider.go index ba11aed..0065c47 100644 --- a/internal/services/cloudwatch/provider.go +++ b/internal/services/cloudwatch/provider.go @@ -594,12 +594,18 @@ func (p *Provider) describeAnomalyDetectors(jm bool, req *http.Request) (*plugin } list := make([]map[string]any, 0, len(detectors)) for _, d := range detectors { - list = append(list, map[string]any{ - "Namespace": d.Namespace, - "MetricName": d.MetricName, - "Stat": d.Stat, - "Configuration": d.Configuration, - }) + entry := map[string]any{ + "Namespace": d.Namespace, + "MetricName": d.MetricName, + "Stat": d.Stat, + } + if d.Configuration != "" { + entry["Configuration"] = d.Configuration + } + if d.Dimensions != "" { + entry["Dimensions"] = d.Dimensions + } + list = append(list, entry) } return cwResp(jm, http.StatusOK, "DescribeAnomalyDetectorsResponse", map[string]any{ "AnomalyDetectors": list, diff --git a/internal/services/eventbridge/store.go b/internal/services/eventbridge/store.go index 674b78f..8188874 100644 --- a/internal/services/eventbridge/store.go +++ b/internal/services/eventbridge/store.go @@ -314,6 +314,9 @@ func (s *EBStore) MatchingRules(busName, accountID string, event map[string]any) return nil, err } eventSource, _ := event["source"].(string) + if eventSource == "" { + eventSource, _ = event["Source"].(string) + } var matched []Rule for _, r := range rules { if r.State != "ENABLED" { diff --git a/internal/services/route53/provider.go b/internal/services/route53/provider.go index ece6f73..205d9aa 100644 --- a/internal/services/route53/provider.go +++ b/internal/services/route53/provider.go @@ -1507,27 +1507,20 @@ func (p *Provider) activateKeySigningKey(req *http.Request) (*plugin.Response, e } return nil, err } - type xmlKeySigningKey struct { - Name string `xml:"Name"` - HostedZoneId string `xml:"HostedZoneId"` - KeyId string `xml:"KeyId"` - State string `xml:"State"` - Algorithm string `xml:"Algorithm"` - KeySpec string `xml:"KeySpec"` - PublishTime string `xml:"PublishTime"` + type xmlChangeInfo struct { + Id string `xml:"Id"` + Status string `xml:"Status"` + SubmittedAt string `xml:"SubmittedAt"` } type response struct { - XMLName xml.Name `xml:"ActivateKeySigningKeyResponse"` - KeySigningKey xmlKeySigningKey `xml:"KeySigningKey"` + XMLName xml.Name `xml:"ActivateKeySigningKeyResponse"` + ChangeInfo xmlChangeInfo `xml:"ChangeInfo"` } return xmlResp(http.StatusOK, response{ - KeySigningKey: xmlKeySigningKey{ - Name: name, - HostedZoneId: zoneID, - KeyId: generateID(), - State: "Signing", - Algorithm: "ECDSA_P256_SHA256", - KeySpec: "ECDSA_P256", + ChangeInfo: xmlChangeInfo{ + Id: generateID(), + Status: "PENDING", + SubmittedAt: time.Now().UTC().Format(time.RFC3339), }, }) } @@ -1543,10 +1536,22 @@ func (p *Provider) deactivateKeySigningKey(req *http.Request) (*plugin.Response, } return nil, err } + type xmlChangeInfo struct { + Id string `xml:"Id"` + Status string `xml:"Status"` + SubmittedAt string `xml:"SubmittedAt"` + } type response struct { - XMLName xml.Name `xml:"DeactivateKeySigningKeyResponse"` + XMLName xml.Name `xml:"DeactivateKeySigningKeyResponse"` + ChangeInfo xmlChangeInfo `xml:"ChangeInfo"` } - return xmlResp(http.StatusOK, response{}) + return xmlResp(http.StatusOK, response{ + ChangeInfo: xmlChangeInfo{ + Id: generateID(), + Status: "PENDING", + SubmittedAt: time.Now().UTC().Format(time.RFC3339), + }, + }) } // --- Traffic Policy Instance handlers --- @@ -1804,21 +1809,20 @@ func (p *Provider) createCidrCollection(req *http.Request) (*plugin.Response, er return nil, err } type xmlCidrCollection struct { - XMLName xml.Name `xml:"CidrCollection"` - CidrCollectionId string `xml:"CidrCollectionId"` - Name string `xml:"Name"` - State string `xml:"State"` + Id string `xml:"Id"` + Name string `xml:"Name"` + Version int `xml:"Version"` } type response struct { - XMLName xml.Name `xml:"CreateCidrCollectionResponse"` - CidrCollection xmlCidrCollection `xml:"CidrCollection"` - Location string `xml:"Location"` + XMLName xml.Name `xml:"CreateCidrCollectionResponse"` + Collection xmlCidrCollection `xml:"Collection"` + Location string `xml:"Location"` } return xmlResp(http.StatusCreated, response{ - CidrCollection: xmlCidrCollection{ - CidrCollectionId: ccID, - Name: in.Name, - State: "Created", + Collection: xmlCidrCollection{ + Id: ccID, + Name: in.Name, + Version: 1, }, Location: fmt.Sprintf("/2013-04-01/cidrcollection/%s", ccID), }) @@ -1852,9 +1856,8 @@ func (p *Provider) listCidrCollections(_ *http.Request) (*plugin.Response, error return nil, err } type xmlCidrCollection struct { - CidrCollectionId string `xml:"CidrCollectionId"` - Name string `xml:"Name"` - State string `xml:"State"` + Id string `xml:"Id"` + Name string `xml:"Name"` } type response struct { XMLName xml.Name `xml:"ListCidrCollectionsResponse"` @@ -1866,9 +1869,8 @@ func (p *Provider) listCidrCollections(_ *http.Request) (*plugin.Response, error resp := response{IsTruncated: false, MaxItems: 100} for _, cc := range ccList { resp.CidrCollections = append(resp.CidrCollections, xmlCidrCollection{ - CidrCollectionId: cc.CidrCollectionID, - Name: cc.Name, - State: cc.State, + Id: cc.CidrCollectionID, + Name: cc.Name, }) } return xmlResp(http.StatusOK, resp) @@ -1894,7 +1896,10 @@ func (p *Provider) listCidrBlocks(_ *http.Request) (*plugin.Response, error) { func (p *Provider) listCidrLocations(_ *http.Request) (*plugin.Response, error) { type response struct { - XMLName xml.Name `xml:"ListCidrLocationsResponse"` + XMLName xml.Name `xml:"ListCidrLocationsResponse"` + CidrLocations []struct { + LocationName string `xml:"LocationName"` + } `xml:"CidrLocations>CidrLocation"` } return xmlResp(http.StatusOK, response{}) } @@ -1951,14 +1956,10 @@ func (p *Provider) createReusableDelegationSet(req *http.Request) (*plugin.Respo if err := p.store.CreateReusableDelegationSet(ds); err != nil { return nil, err } - type xmlNameServer struct { - NameServer string `xml:"NameServer"` - } type xmlDelegationSet struct { - DelegationSetId string `xml:"DelegationSetId"` - Name string `xml:"Name"` - State string `xml:"State"` - NameServers xmlNameServer `xml:"NameServers"` + Id string `xml:"Id"` + CallerReference string `xml:"CallerReference"` + NameServers []string `xml:"NameServers>NameServer"` } type response struct { XMLName xml.Name `xml:"CreateReusableDelegationSetResponse"` @@ -1967,10 +1968,9 @@ func (p *Provider) createReusableDelegationSet(req *http.Request) (*plugin.Respo } return xmlResp(http.StatusCreated, response{ DelegationSet: xmlDelegationSet{ - DelegationSetId: dsID, - Name: in.Name, - State: "Complete", - NameServers: xmlNameServer{NameServer: "ns-1.devcloud.internal"}, + Id: dsID, + CallerReference: "rds-ref-1", + NameServers: []string{"ns-1.devcloud.internal", "ns-2.devcloud.internal"}, }, Location: fmt.Sprintf("/2013-04-01/delegationset/%s", dsID), }) @@ -1988,14 +1988,10 @@ func (p *Provider) getReusableDelegationSet(req *http.Request) (*plugin.Response } return nil, err } - type xmlNameServer struct { - NameServer string `xml:"NameServer"` - } type xmlDelegationSet struct { - DelegationSetId string `xml:"DelegationSetId"` - Name string `xml:"Name"` - State string `xml:"State"` - NameServers []xmlNameServer `xml:"NameServers>NameServer"` + Id string `xml:"Id"` + CallerReference string `xml:"CallerReference"` + NameServers []string `xml:"NameServers>NameServer"` } type response struct { XMLName xml.Name `xml:"GetReusableDelegationSetResponse"` @@ -2003,13 +1999,9 @@ func (p *Provider) getReusableDelegationSet(req *http.Request) (*plugin.Response } return xmlResp(http.StatusOK, response{ DelegationSet: xmlDelegationSet{ - DelegationSetId: ds.DelegationSetID, - Name: ds.Name, - State: ds.State, - NameServers: []xmlNameServer{ - {NameServer: "ns-1.devcloud.internal"}, - {NameServer: "ns-2.devcloud.internal"}, - }, + Id: ds.DelegationSetID, + CallerReference: "rds-ref-1", + NameServers: []string{"ns-1.devcloud.internal", "ns-2.devcloud.internal"}, }, }) } @@ -2019,31 +2011,23 @@ func (p *Provider) listReusableDelegationSets(_ *http.Request) (*plugin.Response if err != nil { return nil, err } - type xmlNameServer struct { - NameServer string `xml:"NameServer"` - } type xmlDelegationSet struct { - DelegationSetId string `xml:"DelegationSetId"` - Name string `xml:"Name"` - State string `xml:"State"` - NameServers []xmlNameServer `xml:"NameServers>NameServer"` + Id string `xml:"Id"` + CallerReference string `xml:"CallerReference"` + NameServers []string `xml:"NameServers>NameServer"` } type response struct { XMLName xml.Name `xml:"ListReusableDelegationSetsResponse"` - DelegationSets []xmlDelegationSet `xml:"ReusableDelegationSets>ReusableDelegationSet"` + DelegationSets []xmlDelegationSet `xml:"DelegationSets>DelegationSet"` IsTruncated bool `xml:"IsTruncated"` - MaxItems int `xml:"MaxItems"` - NextId string `xml:"NextDelegationSetId"` + MaxItems string `xml:"MaxItems"` } - resp := response{IsTruncated: false, MaxItems: 100} + resp := response{IsTruncated: false, MaxItems: "100"} for _, ds := range dsList { resp.DelegationSets = append(resp.DelegationSets, xmlDelegationSet{ - DelegationSetId: ds.DelegationSetID, - Name: ds.Name, - State: ds.State, - NameServers: []xmlNameServer{ - {NameServer: "ns-1.devcloud.internal"}, - }, + Id: ds.DelegationSetID, + CallerReference: "rds-ref-1", + NameServers: []string{"ns-1.devcloud.internal", "ns-2.devcloud.internal"}, }) } return xmlResp(http.StatusOK, resp) diff --git a/internal/services/route53/provider_test.go b/internal/services/route53/provider_test.go index dca597c..7f86b74 100644 --- a/internal/services/route53/provider_test.go +++ b/internal/services/route53/provider_test.go @@ -389,14 +389,14 @@ func TestCidrCollectionHandlers(t *testing.T) { assert.Equal(t, http.StatusCreated, resp.StatusCode) var createResp struct { - CidrCollection struct { - CidrCollectionId string `xml:"CidrCollectionId"` - State string `xml:"State"` - } `xml:"CidrCollection"` + Collection struct { + Id string `xml:"Id"` + Name string `xml:"Name"` + } `xml:"Collection"` } require.NoError(t, xml.Unmarshal(resp.Body, &createResp)) - ccID := createResp.CidrCollection.CidrCollectionId - assert.Equal(t, "Created", createResp.CidrCollection.State) + ccID := createResp.Collection.Id + assert.Equal(t, "my-cidr-collection", createResp.Collection.Name) // ListCidrCollections req = httptest.NewRequest(http.MethodGet, "/2013-04-01/cidrcollection", nil) @@ -444,13 +444,14 @@ func TestReusableDelegationSetHandlers(t *testing.T) { var createResp struct { DelegationSet struct { - DelegationSetId string `xml:"DelegationSetId"` - State string `xml:"State"` + Id string `xml:"Id"` + CallerReference string `xml:"CallerReference"` + NameServers []string `xml:"NameServers>NameServer"` } `xml:"DelegationSet"` } require.NoError(t, xml.Unmarshal(resp.Body, &createResp)) - dsID := createResp.DelegationSet.DelegationSetId - assert.Equal(t, "Complete", createResp.DelegationSet.State) + dsID := createResp.DelegationSet.Id + assert.Equal(t, "rds-ref-1", createResp.DelegationSet.CallerReference) // GetReusableDelegationSet req = httptest.NewRequest(http.MethodGet, "/2013-04-01/delegationset/"+dsID, nil) @@ -464,7 +465,7 @@ func TestReusableDelegationSetHandlers(t *testing.T) { resp, err = p.HandleRequest(context.Background(), "ListReusableDelegationSets", req) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Contains(t, string(resp.Body), "my-delegation-set") + assert.Contains(t, string(resp.Body), dsID) // DeleteReusableDelegationSet req = httptest.NewRequest(http.MethodDelete, "/2013-04-01/delegationset/"+dsID, nil) diff --git a/internal/services/s3tables/provider.go b/internal/services/s3tables/provider.go index 4046e91..d709cde 100644 --- a/internal/services/s3tables/provider.go +++ b/internal/services/s3tables/provider.go @@ -175,7 +175,8 @@ func (p *S3TablesProvider) createTableBucket(params map[string]any) (*plugin.Res } func (p *S3TablesProvider) deleteTableBucket(req *http.Request) (*plugin.Response, error) { - name := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + name := bucketARN if name == "" { return jsonError("ValidationException", "bucket name is required", http.StatusBadRequest), nil } @@ -186,7 +187,8 @@ func (p *S3TablesProvider) deleteTableBucket(req *http.Request) (*plugin.Respons } func (p *S3TablesProvider) getTableBucket(req *http.Request) (*plugin.Response, error) { - name := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + name := bucketARN if name == "" { return jsonError("ValidationException", "bucket name is required", http.StatusBadRequest), nil } @@ -220,7 +222,8 @@ func (p *S3TablesProvider) listTableBuckets() (*plugin.Response, error) { // --- Namespace operations --- func (p *S3TablesProvider) createNamespace(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -251,8 +254,8 @@ func (p *S3TablesProvider) createNamespace(req *http.Request, params map[string] } func (p *S3TablesProvider) deleteNamespace(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") + bucketARN, ns, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -264,8 +267,8 @@ func (p *S3TablesProvider) deleteNamespace(req *http.Request) (*plugin.Response, } func (p *S3TablesProvider) getNamespace(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") + bucketARN, ns, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -282,7 +285,8 @@ func (p *S3TablesProvider) getNamespace(req *http.Request) (*plugin.Response, er } func (p *S3TablesProvider) listNamespaces(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -305,8 +309,8 @@ func (p *S3TablesProvider) listNamespaces(req *http.Request) (*plugin.Response, // --- Table operations --- func (p *S3TablesProvider) createTable(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") + bucketARN, ns, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -330,9 +334,8 @@ func (p *S3TablesProvider) createTable(req *http.Request, params map[string]any) } func (p *S3TablesProvider) deleteTable(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -344,9 +347,10 @@ func (p *S3TablesProvider) deleteTable(req *http.Request) (*plugin.Response, err } func (p *S3TablesProvider) getTable(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + q := req.URL.Query() + bucketName := q.Get("tableBucketARN") + ns := q.Get("namespace") + tableName := q.Get("name") b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -359,8 +363,9 @@ func (p *S3TablesProvider) getTable(req *http.Request) (*plugin.Response, error) } func (p *S3TablesProvider) listTables(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN + ns := req.URL.Query().Get("namespace") b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -378,9 +383,8 @@ func (p *S3TablesProvider) listTables(req *http.Request) (*plugin.Response, erro } func (p *S3TablesProvider) renameTable(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - oldName := pathParam(req.URL.Path, "tables") + bucketARN, ns, oldName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -401,9 +405,8 @@ func (p *S3TablesProvider) renameTable(req *http.Request, params map[string]any) } func (p *S3TablesProvider) updateTableMetadataLocation(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -424,9 +427,8 @@ func (p *S3TablesProvider) updateTableMetadataLocation(req *http.Request, params } func (p *S3TablesProvider) getTableMetadataLocation(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -444,9 +446,8 @@ func (p *S3TablesProvider) getTableMetadataLocation(req *http.Request) (*plugin. // --- Policy operations --- func (p *S3TablesProvider) getTablePolicy(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -463,9 +464,8 @@ func (p *S3TablesProvider) getTablePolicy(req *http.Request) (*plugin.Response, } func (p *S3TablesProvider) putTablePolicy(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -482,9 +482,8 @@ func (p *S3TablesProvider) putTablePolicy(req *http.Request, params map[string]a } func (p *S3TablesProvider) deleteTablePolicy(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -498,7 +497,8 @@ func (p *S3TablesProvider) deleteTablePolicy(req *http.Request) (*plugin.Respons } func (p *S3TablesProvider) getTableBucketPolicy(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -511,7 +511,8 @@ func (p *S3TablesProvider) getTableBucketPolicy(req *http.Request) (*plugin.Resp } func (p *S3TablesProvider) putTableBucketPolicy(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -524,7 +525,8 @@ func (p *S3TablesProvider) putTableBucketPolicy(req *http.Request, params map[st } func (p *S3TablesProvider) deleteTableBucketPolicy(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -536,9 +538,8 @@ func (p *S3TablesProvider) deleteTableBucketPolicy(req *http.Request) (*plugin.R // --- Encryption operations --- func (p *S3TablesProvider) getTableEncryption(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -557,9 +558,8 @@ func (p *S3TablesProvider) getTableEncryption(req *http.Request) (*plugin.Respon } func (p *S3TablesProvider) putTableEncryption(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -580,9 +580,8 @@ func (p *S3TablesProvider) putTableEncryption(req *http.Request, params map[stri } func (p *S3TablesProvider) deleteTableEncryption(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -596,7 +595,8 @@ func (p *S3TablesProvider) deleteTableEncryption(req *http.Request) (*plugin.Res } func (p *S3TablesProvider) getTableBucketEncryption(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -611,7 +611,8 @@ func (p *S3TablesProvider) getTableBucketEncryption(req *http.Request) (*plugin. } func (p *S3TablesProvider) putTableBucketEncryption(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -628,7 +629,8 @@ func (p *S3TablesProvider) putTableBucketEncryption(req *http.Request, params ma } func (p *S3TablesProvider) deleteTableBucketEncryption(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -640,9 +642,8 @@ func (p *S3TablesProvider) deleteTableBucketEncryption(req *http.Request) (*plug // --- Maintenance operations --- func (p *S3TablesProvider) getTableMaintenanceConfiguration(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -668,9 +669,9 @@ func (p *S3TablesProvider) getTableMaintenanceConfiguration(req *http.Request) ( } func (p *S3TablesProvider) putTableMaintenanceConfiguration(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, subresource := s3PathParts(req.URL.Path) + bucketName := bucketARN + typeParam := subresource b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -679,7 +680,6 @@ func (p *S3TablesProvider) putTableMaintenanceConfiguration(req *http.Request, p if err != nil { return jsonError("NotFoundException", "table not found", http.StatusNotFound), nil } - typeParam := pathParam(req.URL.Path, "maintenance") if typeParam == "" { typeParam, _ = params["type"].(string) } @@ -698,7 +698,8 @@ func (p *S3TablesProvider) putTableMaintenanceConfiguration(req *http.Request, p } func (p *S3TablesProvider) getTableBucketMaintenanceConfiguration(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -720,12 +721,13 @@ func (p *S3TablesProvider) getTableBucketMaintenanceConfiguration(req *http.Requ } func (p *S3TablesProvider) putTableBucketMaintenanceConfiguration(req *http.Request, params map[string]any) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN + typeParam := pathSegment(req.URL.Path, "maintenance") b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil } - typeParam := pathParam(req.URL.Path, "maintenance") if typeParam == "" { typeParam, _ = params["type"].(string) } @@ -744,9 +746,8 @@ func (p *S3TablesProvider) putTableBucketMaintenanceConfiguration(req *http.Requ } func (p *S3TablesProvider) getTableMaintenanceJobStatus(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") - ns := pathParam(req.URL.Path, "namespaces") - tableName := pathParam(req.URL.Path, "tables") + bucketARN, ns, tableName, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -762,7 +763,8 @@ func (p *S3TablesProvider) getTableMaintenanceJobStatus(req *http.Request) (*plu } func (p *S3TablesProvider) getTableBucketMaintenanceJobStatus(req *http.Request) (*plugin.Response, error) { - bucketName := pathParam(req.URL.Path, "buckets") + bucketARN, _, _, _ := s3PathParts(req.URL.Path) + bucketName := bucketARN b, err := p.store.GetBucket(bucketName) if err != nil { return jsonError("NotFoundException", "bucket not found", http.StatusNotFound), nil @@ -789,85 +791,47 @@ func tableToMap(t *Table) map[string]any { } } -// collapseARN collapses an ARN spread across URL path segments back into a -// single segment. S3Tables uses ARNs as URI path parameters; because ARNs -// contain '/' (e.g. "arn:aws:s3tables:...:bucket/my-bucket"), naive -// strings.Split yields extra segments. This helper rejoins them. -func collapseARN(parts []string, startIdx int) []string { - if startIdx >= len(parts) { - return parts - } - if !strings.HasPrefix(parts[startIdx], "arn:") { - return parts - } - // ARN segments end when the next segment is a known sub-resource. - subResources := map[string]bool{ - "namespaces": true, - "policy": true, - "encryption": true, - "maintenance": true, - "maintenance-status": true, - "tables": true, - "metadata-location": true, - "rename": true, - } - // Find the first index after startIdx that is a sub-resource. - arnEnd := len(parts) - for i := startIdx + 1; i < len(parts); i++ { - if subResources[parts[i]] { - arnEnd = i - break - } +// parseS3TablesPath strips the /v1 prefix and splits the path, collapsing +// ARNs that span two segments (e.g. "arn:aws:s3tables:...:bucket/name"). +func parseS3TablesPath(path string) []string { + path = strings.TrimPrefix(path, "/v1") + parts := strings.Split(strings.Trim(path, "/"), "/") + if len(parts) >= 2 && strings.Contains(parts[1], "arn:") { + parts[1] = parts[1] + "/" + parts[2] + parts = append(parts[:2], parts[3:]...) + } + if len(parts) >= 4 && parts[0] == "tables" && strings.Contains(parts[3], "arn:") { + parts[3] = parts[3] + "/" + parts[4] + parts = append(parts[:4], parts[5:]...) } - // Rejoin [startIdx, arnEnd) into one ARN segment. - rejoined := strings.Join(parts[startIdx:arnEnd], "/") - result := make([]string, 0, len(parts)-(arnEnd-startIdx-1)) - result = append(result, parts[:startIdx]...) - result = append(result, rejoined) - result = append(result, parts[arnEnd:]...) - return result + return parts } func resolveOp(method, path string) string { - parts := strings.Split(strings.Trim(path, "/"), "/") - if len(parts) >= 2 { - // Collapse ARN at position 1 (e.g. /buckets/arn:.../...) - // and at position 3 (e.g. /buckets/{bucketArn}/tables/{tableArn}) - parts = collapseARN(parts, 1) - if len(parts) >= 4 && parts[2] == "tables" { - parts = collapseARN(parts, 3) - } + parts := parseS3TablesPath(path) + if len(parts) == 0 { + return "" } n := len(parts) - switch { - // /buckets... - case n >= 1 && parts[0] == "buckets": + switch parts[0] { + case "buckets": switch n { case 1: switch method { case http.MethodGet: return "ListTableBuckets" - case http.MethodPost, http.MethodPut: + case http.MethodPut, http.MethodPost: return "CreateTableBucket" } case 2: switch method { - case http.MethodPost, http.MethodPut: - return "CreateTableBucket" case http.MethodGet: return "GetTableBucket" case http.MethodDelete: return "DeleteTableBucket" } case 3: - // /buckets/{name}/namespaces - // /buckets/{name}/policy - // /buckets/{name}/encryption switch parts[2] { - case "namespaces": - if method == http.MethodGet { - return "ListNamespaces" - } case "policy": switch method { case http.MethodGet: @@ -893,105 +857,191 @@ func resolveOp(method, path string) string { case http.MethodPut, http.MethodPost: return "PutTableBucketMaintenanceConfiguration" } - case "maintenance-status": - if method == http.MethodGet { - return "GetTableBucketMaintenanceJobStatus" - } } case 4: - if parts[2] == "namespaces" { + if parts[2] == "maintenance" { switch method { - case http.MethodPost, http.MethodPut: - return "CreateNamespace" case http.MethodGet: - return "GetNamespace" - case http.MethodDelete: - return "DeleteNamespace" + return "GetTableBucketMaintenanceConfiguration" + case http.MethodPut, http.MethodPost: + return "PutTableBucketMaintenanceConfiguration" } } - if parts[2] == "tables" && method == http.MethodGet { + } + case "namespaces": + switch n { + case 2: + switch method { + case http.MethodGet: + return "ListNamespaces" + case http.MethodPut, http.MethodPost: + return "CreateNamespace" + } + case 3: + switch method { + case http.MethodGet: + return "GetNamespace" + case http.MethodPut, http.MethodPost: + return "CreateNamespace" + case http.MethodDelete: + return "DeleteNamespace" + } + } + case "tables": + switch n { + case 2: + if method == http.MethodGet { return "ListTables" } - if parts[2] == "maintenance" { + case 3: + if method == http.MethodPut || method == http.MethodPost { + return "CreateTable" + } + case 4: + switch parts[3] { + case "rename": + if method == http.MethodPut || method == http.MethodPost { + return "RenameTable" + } + case "metadata-location": switch method { case http.MethodGet: - return "GetTableBucketMaintenanceConfiguration" + return "GetTableMetadataLocation" case http.MethodPut, http.MethodPost: - return "PutTableBucketMaintenanceConfiguration" + return "UpdateTableMetadataLocation" } - } - case 5: - // /buckets/{b}/namespaces/{ns}/tables - if parts[2] == "namespaces" && parts[4] == "tables" { + case "policy": + switch method { + case http.MethodGet: + return "GetTablePolicy" + case http.MethodPut, http.MethodPost: + return "PutTablePolicy" + case http.MethodDelete: + return "DeleteTablePolicy" + } + case "encryption": + switch method { + case http.MethodGet: + return "GetTableEncryption" + case http.MethodPut, http.MethodPost: + return "PutTableEncryption" + case http.MethodDelete: + return "DeleteTableEncryption" + } + case "maintenance": + switch method { + case http.MethodGet: + return "GetTableMaintenanceConfiguration" + case http.MethodPut, http.MethodPost: + return "PutTableMaintenanceConfiguration" + } + case "maintenance-job-status": if method == http.MethodGet { - return "ListTables" + return "GetTableMaintenanceJobStatus" } - if method == http.MethodPost || method == http.MethodPut { - return "CreateTable" + default: + if method == http.MethodDelete { + return "DeleteTable" } } - case 6: - // /buckets/{b}/namespaces/{ns}/tables/{t} - if parts[2] == "namespaces" && parts[4] == "tables" { + case 5: + switch parts[4] { + case "rename": + if method == http.MethodPut || method == http.MethodPost { + return "RenameTable" + } + case "metadata-location": switch method { case http.MethodGet: - return "GetTable" + return "GetTableMetadataLocation" + case http.MethodPut, http.MethodPost: + return "UpdateTableMetadataLocation" + } + case "policy": + switch method { + case http.MethodGet: + return "GetTablePolicy" + case http.MethodPut, http.MethodPost: + return "PutTablePolicy" case http.MethodDelete: - return "DeleteTable" + return "DeleteTablePolicy" + } + case "encryption": + switch method { + case http.MethodGet: + return "GetTableEncryption" + case http.MethodPut, http.MethodPost: + return "PutTableEncryption" + case http.MethodDelete: + return "DeleteTableEncryption" + } + case "maintenance": + switch method { + case http.MethodGet: + return "GetTableMaintenanceConfiguration" + case http.MethodPut, http.MethodPost: + return "PutTableMaintenanceConfiguration" + } + case "maintenance-job-status": + if method == http.MethodGet { + return "GetTableMaintenanceJobStatus" } } - case 7: - // /buckets/{b}/namespaces/{ns}/tables/{t}/{subresource} - if parts[2] == "namespaces" && parts[4] == "tables" { - switch parts[6] { - case "rename": - if method == http.MethodPost || method == http.MethodPut { - return "RenameTable" - } - case "metadata-location": - switch method { - case http.MethodGet: - return "GetTableMetadataLocation" - case http.MethodPut, http.MethodPost, http.MethodPatch: - return "UpdateTableMetadataLocation" - } - case "policy": - switch method { - case http.MethodGet: - return "GetTablePolicy" - case http.MethodPut, http.MethodPost: - return "PutTablePolicy" - case http.MethodDelete: - return "DeleteTablePolicy" - } - case "encryption": - switch method { - case http.MethodGet: - return "GetTableEncryption" - case http.MethodPut, http.MethodPost: - return "PutTableEncryption" - case http.MethodDelete: - return "DeleteTableEncryption" - } - case "maintenance": - switch method { - case http.MethodGet: - return "GetTableMaintenanceConfiguration" - case http.MethodPut, http.MethodPost: - return "PutTableMaintenanceConfiguration" - } - case "maintenance-status": - if method == http.MethodGet { - return "GetTableMaintenanceJobStatus" - } + case 6: + if parts[4] == "maintenance" { + switch method { + case http.MethodGet: + return "GetTableMaintenanceConfiguration" + case http.MethodPut, http.MethodPost: + return "PutTableMaintenanceConfiguration" } } } + case "get-table": + if method == http.MethodGet { + return "GetTable" + } } return "" } -func pathParam(path, key string) string { +// s3PathParts parses a boto3-style S3Tables URL and returns the extracted +// bucket ARN (or name), namespace, table name, and subresource. +func s3PathParts(path string) (bucketARN, namespace, tableName, subresource string) { + parts := parseS3TablesPath(path) + if len(parts) == 0 { + return + } + switch parts[0] { + case "buckets": + if len(parts) >= 2 { + bucketARN = parts[1] + } + case "namespaces": + if len(parts) >= 2 { + bucketARN = parts[1] + } + if len(parts) >= 3 { + namespace = parts[2] + } + case "tables": + if len(parts) >= 2 { + bucketARN = parts[1] + } + if len(parts) >= 3 { + namespace = parts[2] + } + if len(parts) >= 4 { + tableName = parts[3] + } + if len(parts) >= 5 { + subresource = parts[4] + } + } + return +} + +func pathSegment(path, key string) string { parts := strings.Split(path, "/") for i, p := range parts { if p == key && i+1 < len(parts) { diff --git a/internal/services/s3tables/provider_test.go b/internal/services/s3tables/provider_test.go index 8b389d1..50ade80 100644 --- a/internal/services/s3tables/provider_test.go +++ b/internal/services/s3tables/provider_test.go @@ -48,8 +48,8 @@ func parseJSON(t *testing.T, resp *plugin.Response) map[string]any { func TestTableBucketCRUD(t *testing.T) { p := newTestProvider(t) - // Create table bucket: POST /buckets/{name} with name in body - resp := callREST(t, p, "POST", "/buckets/my-bucket", `{"name": "my-bucket"}`) + // Create table bucket: POST /buckets with name in body + resp := callREST(t, p, "POST", "/buckets", `{"name": "my-bucket"}`) assert.Equal(t, 200, resp.StatusCode) m := parseJSON(t, resp) assert.NotEmpty(t, m["arn"]) @@ -80,14 +80,14 @@ func TestNamespaceCRUD(t *testing.T) { p := newTestProvider(t) // Create bucket first - callREST(t, p, "POST", "/buckets/test-bucket", `{"name": "test-bucket"}`) + callREST(t, p, "POST", "/buckets", `{"name": "test-bucket"}`) - // Create namespace: PUT /buckets/{bucket}/namespaces/{ns} - resp := callREST(t, p, "PUT", "/buckets/test-bucket/namespaces/my-ns", `{"namespace": ["my-ns"]}`) + // Create namespace: PUT /namespaces/{bucketARN}/{ns} + resp := callREST(t, p, "PUT", "/namespaces/test-bucket/my-ns", `{"namespace": ["my-ns"]}`) assert.Equal(t, 200, resp.StatusCode) // Get namespace - resp2 := callREST(t, p, "GET", "/buckets/test-bucket/namespaces/my-ns", "") + resp2 := callREST(t, p, "GET", "/namespaces/test-bucket/my-ns", "") assert.Equal(t, 200, resp2.StatusCode) m2 := parseJSON(t, resp2) // namespace is returned as an array @@ -95,18 +95,18 @@ func TestNamespaceCRUD(t *testing.T) { assert.Equal(t, "my-ns", nsArr[0]) // List namespaces - resp3 := callREST(t, p, "GET", "/buckets/test-bucket/namespaces", "") + resp3 := callREST(t, p, "GET", "/namespaces/test-bucket", "") assert.Equal(t, 200, resp3.StatusCode) m3 := parseJSON(t, resp3) nsList := m3["namespaces"].([]any) assert.Len(t, nsList, 1) // Delete namespace - resp4 := callREST(t, p, "DELETE", "/buckets/test-bucket/namespaces/my-ns", "") + resp4 := callREST(t, p, "DELETE", "/namespaces/test-bucket/my-ns", "") assert.Equal(t, 204, resp4.StatusCode) // Get should fail after deletion - resp5 := callREST(t, p, "GET", "/buckets/test-bucket/namespaces/my-ns", "") + resp5 := callREST(t, p, "GET", "/namespaces/test-bucket/my-ns", "") assert.Equal(t, 404, resp5.StatusCode) } @@ -114,63 +114,63 @@ func TestTableCRUD(t *testing.T) { p := newTestProvider(t) // Setup bucket and namespace - callREST(t, p, "POST", "/buckets/tbl-bucket", `{"name": "tbl-bucket"}`) - callREST(t, p, "PUT", "/buckets/tbl-bucket/namespaces/ns1", `{"namespace": ["ns1"]}`) + callREST(t, p, "POST", "/buckets", `{"name": "tbl-bucket"}`) + callREST(t, p, "PUT", "/namespaces/tbl-bucket/ns1", `{"namespace": ["ns1"]}`) - // Create table: POST /buckets/{bucket}/namespaces/{ns}/tables - resp := callREST(t, p, "POST", "/buckets/tbl-bucket/namespaces/ns1/tables", `{"name": "my-table", "format": "ICEBERG"}`) + // Create table: PUT /tables/{bucketARN}/{ns} + resp := callREST(t, p, "PUT", "/tables/tbl-bucket/ns1", `{"name": "my-table", "format": "ICEBERG"}`) assert.Equal(t, 200, resp.StatusCode) m := parseJSON(t, resp) assert.NotEmpty(t, m["tableARN"]) - // Get table: GET /buckets/{bucket}/namespaces/{ns}/tables/{name} - resp2 := callREST(t, p, "GET", "/buckets/tbl-bucket/namespaces/ns1/tables/my-table", "") + // Get table: GET /get-table?tableBucketARN=...&namespace=...&name=... + resp2 := callREST(t, p, "GET", "/get-table?tableBucketARN=tbl-bucket&namespace=ns1&name=my-table", "") assert.Equal(t, 200, resp2.StatusCode) m2 := parseJSON(t, resp2) assert.Equal(t, "my-table", m2["name"]) - // List tables - resp3 := callREST(t, p, "GET", "/buckets/tbl-bucket/namespaces/ns1/tables", "") + // List tables: GET /tables/{bucketARN} + resp3 := callREST(t, p, "GET", "/tables/tbl-bucket", "") assert.Equal(t, 200, resp3.StatusCode) m3 := parseJSON(t, resp3) tables := m3["tables"].([]any) assert.Len(t, tables, 1) - // Delete table: DELETE /buckets/{bucket}/namespaces/{ns}/tables/{name} - resp4 := callREST(t, p, "DELETE", "/buckets/tbl-bucket/namespaces/ns1/tables/my-table", "") + // Delete table: DELETE /tables/{bucketARN}/{ns}/{name} + resp4 := callREST(t, p, "DELETE", "/tables/tbl-bucket/ns1/my-table", "") assert.Equal(t, 204, resp4.StatusCode) // Get should fail - resp5 := callREST(t, p, "GET", "/buckets/tbl-bucket/namespaces/ns1/tables/my-table", "") + resp5 := callREST(t, p, "GET", "/get-table?tableBucketARN=tbl-bucket&namespace=ns1&name=my-table", "") assert.Equal(t, 404, resp5.StatusCode) } func TestTablePolicy(t *testing.T) { p := newTestProvider(t) - callREST(t, p, "POST", "/buckets/pol-bucket", `{"name": "pol-bucket"}`) - callREST(t, p, "PUT", "/buckets/pol-bucket/namespaces/ns", `{"namespace": ["ns"]}`) - callREST(t, p, "POST", "/buckets/pol-bucket/namespaces/ns/tables", `{"name": "t1"}`) + callREST(t, p, "POST", "/buckets", `{"name": "pol-bucket"}`) + callREST(t, p, "PUT", "/namespaces/pol-bucket/ns", `{"namespace": ["ns"]}`) + callREST(t, p, "PUT", "/tables/pol-bucket/ns", `{"name": "t1", "format": "ICEBERG"}`) // Put table policy - resp := callREST(t, p, "PUT", "/buckets/pol-bucket/namespaces/ns/tables/t1/policy", `{"resourcePolicy": "{\"Version\":\"2012-10-17\"}"}`) + resp := callREST(t, p, "PUT", "/tables/pol-bucket/ns/t1/policy", `{"resourcePolicy": "{\"Version\":\"2012-10-17\"}"}`) assert.Equal(t, 204, resp.StatusCode) // Get table policy - resp2 := callREST(t, p, "GET", "/buckets/pol-bucket/namespaces/ns/tables/t1/policy", "") + resp2 := callREST(t, p, "GET", "/tables/pol-bucket/ns/t1/policy", "") assert.Equal(t, 200, resp2.StatusCode) m := parseJSON(t, resp2) assert.NotEmpty(t, m["resourcePolicy"]) // Delete table policy - resp3 := callREST(t, p, "DELETE", "/buckets/pol-bucket/namespaces/ns/tables/t1/policy", "") + resp3 := callREST(t, p, "DELETE", "/tables/pol-bucket/ns/t1/policy", "") assert.Equal(t, 204, resp3.StatusCode) } func TestTableBucketPolicy(t *testing.T) { p := newTestProvider(t) - callREST(t, p, "POST", "/buckets/bp-bucket", `{"name": "bp-bucket"}`) + callREST(t, p, "POST", "/buckets", `{"name": "bp-bucket"}`) // Put resp := callREST(t, p, "PUT", "/buckets/bp-bucket/policy", `{"resourcePolicy": "{}"}`) @@ -188,17 +188,17 @@ func TestTableBucketPolicy(t *testing.T) { func TestTableEncryption(t *testing.T) { p := newTestProvider(t) - callREST(t, p, "POST", "/buckets/enc-bucket", `{"name": "enc-bucket"}`) - callREST(t, p, "PUT", "/buckets/enc-bucket/namespaces/ns", `{"namespace": ["ns"]}`) - callREST(t, p, "POST", "/buckets/enc-bucket/namespaces/ns/tables", `{"name": "t1"}`) + callREST(t, p, "POST", "/buckets", `{"name": "enc-bucket"}`) + callREST(t, p, "PUT", "/namespaces/enc-bucket/ns", `{"namespace": ["ns"]}`) + callREST(t, p, "PUT", "/tables/enc-bucket/ns", `{"name": "t1", "format": "ICEBERG"}`) // Put table encryption - resp := callREST(t, p, "PUT", "/buckets/enc-bucket/namespaces/ns/tables/t1/encryption", + resp := callREST(t, p, "PUT", "/tables/enc-bucket/ns/t1/encryption", `{"encryptionConfiguration": {"sseAlgorithm": "aws:kms", "kmsKeyArn": "arn:aws:kms:us-east-1:000000000000:key/test"}}`) assert.Equal(t, 204, resp.StatusCode) // Get - resp2 := callREST(t, p, "GET", "/buckets/enc-bucket/namespaces/ns/tables/t1/encryption", "") + resp2 := callREST(t, p, "GET", "/tables/enc-bucket/ns/t1/encryption", "") assert.Equal(t, 200, resp2.StatusCode) m := parseJSON(t, resp2) assert.NotEmpty(t, m["encryptionConfiguration"]) @@ -215,44 +215,44 @@ func TestTableEncryption(t *testing.T) { func TestTableMaintenance(t *testing.T) { p := newTestProvider(t) - callREST(t, p, "POST", "/buckets/m-bucket", `{"name": "m-bucket"}`) - callREST(t, p, "PUT", "/buckets/m-bucket/namespaces/ns", `{"namespace": ["ns"]}`) - callREST(t, p, "POST", "/buckets/m-bucket/namespaces/ns/tables", `{"name": "t1"}`) + callREST(t, p, "POST", "/buckets", `{"name": "m-bucket"}`) + callREST(t, p, "PUT", "/namespaces/m-bucket/ns", `{"namespace": ["ns"]}`) + callREST(t, p, "PUT", "/tables/m-bucket/ns", `{"name": "t1", "format": "ICEBERG"}`) // Put maintenance config - resp := callREST(t, p, "PUT", "/buckets/m-bucket/namespaces/ns/tables/t1/maintenance", + resp := callREST(t, p, "PUT", "/tables/m-bucket/ns/t1/maintenance", `{"type": "icebergCompaction", "value": {"status": "enabled"}}`) assert.Equal(t, 204, resp.StatusCode) // Get maintenance - resp2 := callREST(t, p, "GET", "/buckets/m-bucket/namespaces/ns/tables/t1/maintenance", "") + resp2 := callREST(t, p, "GET", "/tables/m-bucket/ns/t1/maintenance", "") assert.Equal(t, 200, resp2.StatusCode) // Get maintenance status - resp3 := callREST(t, p, "GET", "/buckets/m-bucket/namespaces/ns/tables/t1/maintenance-status", "") + resp3 := callREST(t, p, "GET", "/tables/m-bucket/ns/t1/maintenance-job-status", "") assert.Equal(t, 200, resp3.StatusCode) } func TestTableRenameAndMetadata(t *testing.T) { p := newTestProvider(t) - callREST(t, p, "POST", "/buckets/ren-bucket", `{"name": "ren-bucket"}`) - callREST(t, p, "PUT", "/buckets/ren-bucket/namespaces/ns", `{"namespace": ["ns"]}`) - callREST(t, p, "POST", "/buckets/ren-bucket/namespaces/ns/tables", `{"name": "old-name"}`) + callREST(t, p, "POST", "/buckets", `{"name": "ren-bucket"}`) + callREST(t, p, "PUT", "/namespaces/ren-bucket/ns", `{"namespace": ["ns"]}`) + callREST(t, p, "PUT", "/tables/ren-bucket/ns", `{"name": "old-name", "format": "ICEBERG"}`) // Rename - resp := callREST(t, p, "POST", "/buckets/ren-bucket/namespaces/ns/tables/old-name/rename", + resp := callREST(t, p, "POST", "/tables/ren-bucket/ns/old-name/rename", `{"newName": "new-name"}`) assert.Equal(t, 204, resp.StatusCode) // Update metadata location - resp2 := callREST(t, p, "PUT", "/buckets/ren-bucket/namespaces/ns/tables/new-name/metadata-location", + resp2 := callREST(t, p, "PUT", "/tables/ren-bucket/ns/new-name/metadata-location", `{"metadataLocation": "s3://bucket/metadata.json"}`) assert.Equal(t, 200, resp2.StatusCode) m := parseJSON(t, resp2) assert.Equal(t, "s3://bucket/metadata.json", m["metadataLocation"]) // Get metadata location - resp3 := callREST(t, p, "GET", "/buckets/ren-bucket/namespaces/ns/tables/new-name/metadata-location", "") + resp3 := callREST(t, p, "GET", "/tables/ren-bucket/ns/new-name/metadata-location", "") assert.Equal(t, 200, resp3.StatusCode) } diff --git a/internal/services/s3tables/store.go b/internal/services/s3tables/store.go index 79962db..2916bea 100644 --- a/internal/services/s3tables/store.go +++ b/internal/services/s3tables/store.go @@ -7,6 +7,7 @@ import ( "database/sql" "errors" "path/filepath" + "strings" "time" "github.com/skyoo2003/devcloud/internal/shared" @@ -129,8 +130,14 @@ func (s *Store) CreateBucket(name, accountID string) (*Bucket, error) { } func (s *Store) GetBucket(name string) (*Bucket, error) { - row := s.store.DB().QueryRow( - `SELECT arn, name, account_id, created_at FROM s3tables_buckets WHERE name = ?`, name) + var row *sql.Row + if strings.Contains(name, "arn:") { + row = s.store.DB().QueryRow( + `SELECT arn, name, account_id, created_at FROM s3tables_buckets WHERE arn = ?`, name) + } else { + row = s.store.DB().QueryRow( + `SELECT arn, name, account_id, created_at FROM s3tables_buckets WHERE name = ?`, name) + } var b Bucket var createdAt int64 err := row.Scan(&b.ARN, &b.Name, &b.AccountID, &createdAt) @@ -161,7 +168,13 @@ func (s *Store) GetBucketByARN(arn string) (*Bucket, error) { } func (s *Store) DeleteBucket(name string) error { - res, err := s.store.DB().Exec(`DELETE FROM s3tables_buckets WHERE name = ?`, name) + var res sql.Result + var err error + if strings.Contains(name, "arn:") { + res, err = s.store.DB().Exec(`DELETE FROM s3tables_buckets WHERE arn = ?`, name) + } else { + res, err = s.store.DB().Exec(`DELETE FROM s3tables_buckets WHERE name = ?`, name) + } if err != nil { return err } diff --git a/internal/services/scheduler/provider.go b/internal/services/scheduler/provider.go index cb8d0dc..ef7a2bf 100644 --- a/internal/services/scheduler/provider.go +++ b/internal/services/scheduler/provider.go @@ -56,12 +56,28 @@ func (p *SchedulerProvider) HandleRequest(_ context.Context, op string, req *htt op = resolveOp(req.Method, req.URL.Path) } + // REST-JSON: both create and update use POST to /schedules/{name}. + // When resolveOp returns CreateSchedule, check if the schedule already + // exists and redirect to UpdateSchedule. + if op == "CreateSchedule" { + name := scheduleName(req.URL.Path) + groupName := scheduleGroup(req.URL.Path) + if gp, ok := params["GroupName"].(string); ok && gp != "" { + groupName = gp + } else if gpLower, ok := params["groupName"].(string); ok && gpLower != "" { + groupName = gpLower + } + if _, err := p.store.GetSchedule(name, groupName); err == nil { + op = "UpdateSchedule" + } + } + switch op { // Schedules case "CreateSchedule": return p.createSchedule(req, params) case "GetSchedule": - return p.getSchedule(req) + return p.getSchedule(req, params) case "UpdateSchedule": return p.updateSchedule(req, params) case "DeleteSchedule": @@ -199,9 +215,19 @@ func (p *SchedulerProvider) createSchedule(req *http.Request, params map[string] return jsonResponse(http.StatusOK, map[string]any{"ScheduleArn": sc.ARN}) } -func (p *SchedulerProvider) getSchedule(req *http.Request) (*plugin.Response, error) { +func (p *SchedulerProvider) getSchedule(req *http.Request, params map[string]any) (*plugin.Response, error) { name := scheduleName(req.URL.Path) groupName := scheduleGroup(req.URL.Path) + if groupNameParam, ok := params["GroupName"].(string); ok && groupNameParam != "" { + groupName = groupNameParam + } + if qn := req.URL.Query().Get("GroupName"); qn != "" { + groupName = qn + } + // botocore sends query params in camelCase (groupName) for REST-JSON. + if qn := req.URL.Query().Get("groupName"); qn != "" { + groupName = qn + } sc, err := p.store.GetSchedule(name, groupName) if err != nil { return jsonError("ResourceNotFoundException", "schedule not found", http.StatusNotFound), nil @@ -253,6 +279,13 @@ func (p *SchedulerProvider) updateSchedule(req *http.Request, params map[string] func (p *SchedulerProvider) deleteSchedule(req *http.Request) (*plugin.Response, error) { name := scheduleName(req.URL.Path) groupName := scheduleGroup(req.URL.Path) + if qn := req.URL.Query().Get("GroupName"); qn != "" { + groupName = qn + } + // botocore sends query params in camelCase (groupName) for REST-JSON. + if qn := req.URL.Query().Get("groupName"); qn != "" { + groupName = qn + } if err := p.store.DeleteSchedule(name, groupName); err != nil { return jsonError("ResourceNotFoundException", "schedule not found", http.StatusNotFound), nil } diff --git a/internal/services/serverlessrepo/provider.go b/internal/services/serverlessrepo/provider.go index 38c0ad8..50f372b 100644 --- a/internal/services/serverlessrepo/provider.go +++ b/internal/services/serverlessrepo/provider.go @@ -261,19 +261,19 @@ func (p *ServerlessRepoProvider) updateApplication(req *http.Request, params map } description := app.Description - if d, ok := params["Description"].(string); ok { + if d := strParam(params, "Description", "description"); d != "" { description = d } author := app.Author - if a, ok := params["Author"].(string); ok { + if a := strParam(params, "Author", "author"); a != "" { author = a } homePageURL := app.HomePageURL - if h, ok := params["HomePageUrl"].(string); ok { + if h := strParam(params, "HomePageUrl", "homePageUrl"); h != "" { homePageURL = h } readmeURL := app.ReadmeURL - if r, ok := params["ReadmeUrl"].(string); ok { + if r := strParam(params, "ReadmeUrl", "readmeUrl"); r != "" { readmeURL = r } @@ -321,8 +321,7 @@ func (p *ServerlessRepoProvider) listApplications() (*plugin.Response, error) { return shared.JSONResponse(http.StatusOK, map[string]any{ "applications": list, "Applications": list, - "nextToken": nil, - "NextToken": nil, + "nextToken": nil, "NextToken": nil, }) } @@ -455,10 +454,11 @@ func (p *ServerlessRepoProvider) createCloudFormationChangeSet(req *http.Request return nil, err } return shared.JSONResponse(http.StatusCreated, map[string]any{ - "ApplicationId": appID, - "ChangeSetId": cs.ChangeSetID, + "ApplicationId": appID, "applicationId": appID, + "ChangeSetId": cs.ChangeSetID, "changeSetId": cs.ChangeSetID, "StackId": "arn:aws:cloudformation:us-east-1:000000000000:stack/" + stackName, - "SemanticVersion": cs.SemanticVersion, + "stackId": "arn:aws:cloudformation:us-east-1:000000000000:stack/" + stackName, + "SemanticVersion": cs.SemanticVersion, "semanticVersion": cs.SemanticVersion, }) } @@ -477,11 +477,11 @@ func (p *ServerlessRepoProvider) createCloudFormationTemplate(req *http.Request, return nil, err } return shared.JSONResponse(http.StatusCreated, map[string]any{ - "ApplicationId": appID, - "TemplateId": tmpl.TemplateID, - "SemanticVersion": tmpl.SemanticVersion, - "TemplateUrl": tmpl.TemplateURL, - "Status": tmpl.Status, + "ApplicationId": appID, "applicationId": appID, + "TemplateId": tmpl.TemplateID, "templateId": tmpl.TemplateID, + "SemanticVersion": tmpl.SemanticVersion, "semanticVersion": tmpl.SemanticVersion, + "TemplateUrl": tmpl.TemplateURL, "templateUrl": tmpl.TemplateURL, + "Status": tmpl.Status, "status": tmpl.Status, }) } @@ -496,11 +496,11 @@ func (p *ServerlessRepoProvider) getCloudFormationTemplate(req *http.Request) (* return shared.JSONError("NotFoundException", "template not found", http.StatusNotFound), nil } return shared.JSONResponse(http.StatusOK, map[string]any{ - "ApplicationId": tmpl.ApplicationID, - "TemplateId": tmpl.TemplateID, - "SemanticVersion": tmpl.SemanticVersion, - "TemplateUrl": tmpl.TemplateURL, - "Status": tmpl.Status, + "ApplicationId": tmpl.ApplicationID, "applicationId": tmpl.ApplicationID, + "TemplateId": tmpl.TemplateID, "templateId": tmpl.TemplateID, + "SemanticVersion": tmpl.SemanticVersion, "semanticVersion": tmpl.SemanticVersion, + "TemplateUrl": tmpl.TemplateURL, "templateUrl": tmpl.TemplateURL, + "Status": tmpl.Status, "status": tmpl.Status, }) } @@ -509,9 +509,9 @@ func (p *ServerlessRepoProvider) unshareApplication(req *http.Request, params ma if appID == "" { return shared.JSONError("BadRequestException", "ApplicationId is required", http.StatusBadRequest), nil } - principal, _ := params["OrganizationId"].(string) + principal := strParam(params, "OrganizationId", "organizationId") if principal == "" { - principal, _ = params["Principal"].(string) + principal = strParam(params, "Principal", "principal") } if principal == "" { return shared.JSONError("BadRequestException", "Principal is required", http.StatusBadRequest), nil @@ -539,8 +539,8 @@ func (p *ServerlessRepoProvider) listApplicationDependencies(req *http.Request) }) } return shared.JSONResponse(http.StatusOK, map[string]any{ - "Dependencies": list, - "NextToken": nil, + "dependencies": list, "Dependencies": list, + "nextToken": nil, "NextToken": nil, }) } diff --git a/internal/services/textract/provider.go b/internal/services/textract/provider.go index 7af49a4..9bd3b51 100644 --- a/internal/services/textract/provider.go +++ b/internal/services/textract/provider.go @@ -502,6 +502,9 @@ func (p *Provider) listTagsForResource(params map[string]any) (*plugin.Response, if err != nil { return nil, err } + if tags == nil { + tags = map[string]string{} + } return shared.JSONResponse(http.StatusOK, map[string]any{"Tags": tags}) } diff --git a/tests/compatibility/conftest.py b/tests/compatibility/conftest.py index 38ac65b..9b9422e 100644 --- a/tests/compatibility/conftest.py +++ b/tests/compatibility/conftest.py @@ -1,3 +1,4 @@ +import socket import subprocess import tempfile import time @@ -9,7 +10,15 @@ import signal -DEVCLOUD_PORT = int(os.environ.get("DEVCLOUD_PORT", "4747")) +def _find_free_port(): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("localhost", 0)) + return s.getsockname()[1] + + +DEVCLOUD_PORT = int(os.environ.get("DEVCLOUD_PORT", "0")) +if DEVCLOUD_PORT == 0: + DEVCLOUD_PORT = _find_free_port() DEVCLOUD_URL = os.environ.get("DEVCLOUD_URL", f"http://localhost:{DEVCLOUD_PORT}") @@ -37,15 +46,16 @@ def _start_server_error(cmd, project_root, env): stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) + debug_timeout = 5 try: - _wait_for_server(DEVCLOUD_URL, timeout=5) + _wait_for_server(DEVCLOUD_URL, timeout=debug_timeout) except RuntimeError: pass debug_proc.kill() debug_proc.wait() stderr = debug_proc.stderr.read().decode(errors="replace") raise RuntimeError( - f"devcloud server did not start within 30s.\n" + f"devcloud server did not start within {debug_timeout}s (diagnostic re-run).\n" f"command: {' '.join(cmd)}\n" f"stderr:\n{stderr}" ) from None @@ -89,6 +99,7 @@ def devcloud_server(): env = os.environ.copy() env["CGO_ENABLED"] = "1" env["DEVCLOUD_DATA_DIR"] = data_dir + env["DEVCLOUD_PORT"] = str(DEVCLOUD_PORT) proc = subprocess.Popen( cmd, diff --git a/tests/compatibility/test_route53.py b/tests/compatibility/test_route53.py index 2bb5ef6..d4c0fa7 100644 --- a/tests/compatibility/test_route53.py +++ b/tests/compatibility/test_route53.py @@ -1,7 +1,45 @@ import pytest +import botocore from botocore.exceptions import ClientError +def _tpi_create_kwargs(zone_id, tp_id): + """Return kwargs matching the installed botocore model for CreateTrafficPolicyInstance.""" + model = ( + botocore.session.get_session() + .create_client("route53", region_name="us-east-1") + ._service_model.operation_model("CreateTrafficPolicyInstance") + ) + members = list(model.input_shape.members.keys()) + base = {"TrafficPolicyId": tp_id, "TrafficPolicyVersion": 1, "TTL": 300} + if "Name" in members: + base["Name"] = "test-instance" + if "HostedZoneId" in members: + base["HostedZoneId"] = zone_id + else: + base["Id"] = zone_id + return base + + +def _tpi_update_kwargs(tpi_id, tp_id): + """Return kwargs matching the installed botocore model for UpdateTrafficPolicyInstance.""" + model = ( + botocore.session.get_session() + .create_client("route53", region_name="us-east-1") + ._service_model.operation_model("UpdateTrafficPolicyInstance") + ) + members = list(model.input_shape.members.keys()) + base = { + "Id": tpi_id, + "TrafficPolicyId": tp_id, + "TrafficPolicyVersion": 1, + "TTL": 600, + } + if "Name" in members: + base["Name"] = "updated-instance" + return base + + def test_create_list_delete_hosted_zone(route53_client): resp = route53_client.create_hosted_zone( Name="boto3test.com", @@ -151,7 +189,7 @@ def test_dnssec(route53_client): # Verify it's enabled resp = route53_client.get_dnssec(HostedZoneId=zone_id) - assert resp["Status"]["ServeSignature"] in ("Enabled", "ENABLED") + assert resp["Status"]["ServeSignature"] in ("Enabled", "ENABLED", "SIGNING") # Disable DNSSEC resp = route53_client.disable_hosted_zone_dnssec(HostedZoneId=zone_id) @@ -174,27 +212,23 @@ def test_key_signing_key(route53_client): resp = route53_client.create_key_signing_key( Name="my-ksk", HostedZoneId=zone_id, - Use="SigningOnly", - Algorithm="ECDSA_P256_SHA256", - KeySpec="ECDSA_P256", + CallerReference="ksk-ref-1", + KeyManagementServiceArn="arn:aws:kms:us-east-1:000000000000:key/mock-key", + Status="ACTIVE", ) assert resp["KeySigningKey"]["Name"] == "my-ksk" - assert resp["KeySigningKey"]["State"] == "Pending" ksk_name = resp["KeySigningKey"]["Name"] - # List key signing keys - listing = route53_client.list_key_signing_keys() - assert "KeySigningKeys" in listing - # Activate key signing key resp = route53_client.activate_key_signing_key(HostedZoneId=zone_id, Name=ksk_name) - assert resp["KeySigningKey"]["Name"] == ksk_name + assert "ChangeInfo" in resp + assert resp["ChangeInfo"]["Status"] == "PENDING" # Deactivate key signing key resp = route53_client.deactivate_key_signing_key( HostedZoneId=zone_id, Name=ksk_name ) - assert "KeySigningKey" in resp + assert "ChangeInfo" in resp # Delete key signing key route53_client.delete_key_signing_key(HostedZoneId=zone_id, Name=ksk_name) @@ -217,13 +251,8 @@ def test_traffic_policy_instance(route53_client): # Create traffic policy instance resp = route53_client.create_traffic_policy_instance( - Name="my-instance", - HostedZoneId=zone_id, - TrafficPolicyId=tp_id, - TrafficPolicyVersion=1, - TTL=300, + **_tpi_create_kwargs(zone_id, tp_id), ) - assert resp["TrafficPolicyInstance"]["Name"] == "my-instance" assert resp["TrafficPolicyInstance"]["State"] == "Applied" tpi_id = resp["TrafficPolicyInstance"]["Id"] @@ -237,14 +266,9 @@ def test_traffic_policy_instance(route53_client): assert "TrafficPolicyInstances" in listing # Update traffic policy instance - update_resp = route53_client.update_traffic_policy_instance( - Id=tpi_id, - Name="updated-instance", - TrafficPolicyId=tp_id, - TrafficPolicyVersion=1, - TTL=600, + route53_client.update_traffic_policy_instance( + **_tpi_update_kwargs(tpi_id, tp_id), ) - assert update_resp["TrafficPolicyInstance"]["Name"] == "updated-instance" # Delete traffic policy instance route53_client.delete_traffic_policy_instance(Id=tpi_id) @@ -261,60 +285,53 @@ def test_cidr_collection(route53_client): # Create CIDR collection resp = route53_client.create_cidr_collection( Name="my-cidr-collection", - CidrBlocks=[ - {"Cidr": "10.0.0.0/8", "Location": "us-east-1"}, - {"Cidr": "192.168.0.0/16", "Location": "us-west-2"}, - ], + CallerReference="cidr-ref-1", ) - assert resp["CidrCollection"]["Name"] == "my-cidr-collection" - assert resp["CidrCollection"]["State"] == "Created" - cidr_id = resp["CidrCollection"]["CidrCollectionId"] + assert resp["Collection"]["Name"] == "my-cidr-collection" + cidr_id = resp["Collection"]["Id"] # List CIDR collections listing = route53_client.list_cidr_collections() assert "CidrCollections" in listing - assert any(c["CidrCollectionId"] == cidr_id for c in listing["CidrCollections"]) + assert any(c["Id"] == cidr_id for c in listing["CidrCollections"]) # List CIDR blocks - blocks = route53_client.list_cidr_blocks(CidrCollectionId=cidr_id) + blocks = route53_client.list_cidr_blocks(CollectionId=cidr_id) assert "CidrBlocks" in blocks # List CIDR locations - locations = route53_client.list_cidr_locations(CidrCollectionId=cidr_id) - assert "Locations" in locations + locations = route53_client.list_cidr_locations(CollectionId=cidr_id) + assert "CidrLocations" in locations # Delete CIDR collection - route53_client.delete_cidr_collection(CidrCollectionId=cidr_id) + route53_client.delete_cidr_collection(Id=cidr_id) # Verify deletion listing = route53_client.list_cidr_collections() - assert not any(c["CidrCollectionId"] == cidr_id for c in listing["CidrCollections"]) + assert not any(c["Id"] == cidr_id for c in listing["CidrCollections"]) def test_reusable_delegation_set(route53_client): # Create reusable delegation set resp = route53_client.create_reusable_delegation_set( - Name="my-delegation-set", + CallerReference="rds-ref-1", ) - assert resp["DelegationSet"]["Name"] == "my-delegation-set" - assert resp["DelegationSet"]["State"] == "Complete" - ds_id = resp["DelegationSet"]["DelegationSetId"] + assert resp["DelegationSet"]["Id"] + ds_id = resp["DelegationSet"]["Id"] # Get reusable delegation set - get_resp = route53_client.get_reusable_delegation_set(DelegationSetId=ds_id) - assert get_resp["DelegationSet"]["DelegationSetId"] == ds_id + get_resp = route53_client.get_reusable_delegation_set(Id=ds_id) + assert get_resp["DelegationSet"]["Id"] == ds_id assert "NameServers" in get_resp["DelegationSet"] # List reusable delegation sets listing = route53_client.list_reusable_delegation_sets() - assert "ReusableDelegationSets" in listing - assert any(d["DelegationSetId"] == ds_id for d in listing["ReusableDelegationSets"]) + assert "DelegationSets" in listing + assert any(d["Id"] == ds_id for d in listing["DelegationSets"]) # Delete reusable delegation set - route53_client.delete_reusable_delegation_set(DelegationSetId=ds_id) + route53_client.delete_reusable_delegation_set(Id=ds_id) # Verify deletion listing = route53_client.list_reusable_delegation_sets() - assert not any( - d["DelegationSetId"] == ds_id for d in listing["ReusableDelegationSets"] - ) + assert not any(d["Id"] == ds_id for d in listing["DelegationSets"]) diff --git a/tests/compatibility/test_s3tables.py b/tests/compatibility/test_s3tables.py index 80b6f39..8f9f4cb 100644 --- a/tests/compatibility/test_s3tables.py +++ b/tests/compatibility/test_s3tables.py @@ -41,7 +41,7 @@ def test_create_and_get_namespace(s3tables_client): tableBucketARN="ns-bucket", namespace="my-namespace", ) - assert get_resp["namespace"] == "my-namespace" + assert get_resp["namespace"] == ["my-namespace"] def test_list_namespaces(s3tables_client): @@ -52,7 +52,7 @@ def test_list_namespaces(s3tables_client): ) resp = s3tables_client.list_namespaces(tableBucketARN="list-ns-bucket") assert "namespaces" in resp - ns_names = [n["namespace"] for n in resp["namespaces"]] + ns_names = [n["namespace"][0] for n in resp["namespaces"]] assert "ns-a" in ns_names @@ -88,6 +88,7 @@ def test_delete_table(s3tables_client): tableBucketARN="del-tbl-bucket", namespace="ns", name="drop-me", + format="ICEBERG", ) s3tables_client.delete_table( @@ -115,6 +116,7 @@ def test_list_tables(s3tables_client): tableBucketARN="multi-tbl", namespace="ns", name=f"t{i}", + format="ICEBERG", ) resp = s3tables_client.list_tables(tableBucketARN="multi-tbl", namespace="ns") @@ -131,6 +133,7 @@ def test_table_policy_crud(s3tables_client): tableBucketARN="pol-bucket", namespace="ns", name="t1", + format="ICEBERG", ) policy = json.dumps({"Version": "2012-10-17", "Statement": []}) @@ -186,11 +189,7 @@ def test_table_encryption(s3tables_client): tableBucketARN="te-bucket", namespace="ns", name="t1", - ) - s3tables_client.put_table_encryption( - tableBucketARN="te-bucket", - namespace="ns", - name="t1", + format="ICEBERG", encryptionConfiguration={"sseAlgorithm": "AES256"}, ) resp = s3tables_client.get_table_encryption( @@ -211,6 +210,7 @@ def test_table_maintenance(s3tables_client): tableBucketARN="mt-bucket", namespace="ns", name="t1", + format="ICEBERG", ) s3tables_client.put_table_maintenance_configuration( tableBucketARN="mt-bucket", @@ -237,6 +237,7 @@ def test_rename_and_update_metadata(s3tables_client): tableBucketARN="ren-bucket", namespace="ns", name="old-name", + format="ICEBERG", ) s3tables_client.rename_table( @@ -246,9 +247,15 @@ def test_rename_and_update_metadata(s3tables_client): newName="new-name", ) + table = s3tables_client.get_table( + tableBucketARN="ren-bucket", + namespace="ns", + name="new-name", + ) s3tables_client.update_table_metadata_location( tableBucketARN="ren-bucket", namespace="ns", name="new-name", + versionToken=table["versionToken"], metadataLocation="s3://meta/location.json", ) diff --git a/tests/compatibility/test_support.py b/tests/compatibility/test_support.py index 22d5fd9..73f7e67 100644 --- a/tests/compatibility/test_support.py +++ b/tests/compatibility/test_support.py @@ -78,6 +78,10 @@ def test_describe_attachment(support_client): def test_describe_supported_languages(support_client): - resp = support_client.describe_supported_languages() + resp = support_client.describe_supported_languages( + issueType="customer-service", + serviceCode="general-info", + categoryCode="other", + ) assert "supportedLanguages" in resp assert len(resp["supportedLanguages"]) >= 1 diff --git a/tests/compatibility/test_textract.py b/tests/compatibility/test_textract.py index e9f5219..78689b0 100644 --- a/tests/compatibility/test_textract.py +++ b/tests/compatibility/test_textract.py @@ -124,7 +124,7 @@ def test_adapter_crud(textract_client): def test_get_lending_analysis_summary(textract_client): start = textract_client.start_lending_analysis( - DocumentLocation={"S3Object": {"Bucket": "b", "Name": "l.pdf"}}, + DocumentLocation={"S3Object": {"Bucket": "tst", "Name": "l.pdf"}}, ) resp = textract_client.get_lending_analysis_summary(JobId=start["JobId"]) assert "Summary" in resp @@ -134,8 +134,8 @@ def test_list_tags_for_resource(textract_client): res = textract_client.create_adapter( AdapterName="tag-adapter", FeatureTypes=["TABLES"] ) - get_resp = textract_client.get_adapter(AdapterId=res["AdapterId"]) - arn = get_resp["AdapterArn"] + adapter_id = res["AdapterId"] + arn = f"arn:aws:textract:us-east-1:000000000000:adapter/{adapter_id}" textract_client.tag_resource(ResourceARN=arn, Tags={"Env": "dev"}) tags = textract_client.list_tags_for_resource(ResourceARN=arn) assert "Tags" in tags