From b9127b85b33e58f30444b7afcf87e3d85b261c62 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Thu, 30 Jul 2026 07:59:37 +0900 Subject: [PATCH 1/8] refactor: cut dead plugin surface and de-duplicate provider helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A repo-wide over-engineering audit turned up surface that no code path can reach and helpers copied per service. Remove the GetMetrics/ServiceMetrics plugin API: 102 of 104 implementations returned a zero-valued struct, and TotalRequests/ErrorCount were never populated by anyone, so /devcloud/api/metrics reported zeros forever. The two services that did fill ResourceCount (DynamoDB, Lambda) are unaffected in practice — /devcloud/api/services already derives resource counts from ListResources. The interface is still pre-1.0, so this is not a v1.x break. Delete the generic shared.ResourceStore (114 lines plus 204 of tests): no production caller ever used it, every service writes its own SQL. Its shared.Scanner was a second declaration of the identical sqlite.Scanner, so configservice now uses the latter. Fold 23 copies of the same string-param accessor (strParam / strVal / getString / str — three spellings, identical behaviour) into shared.StrParam, point 10 per-service region constants at shared.DefaultRegion, and make shared.DefaultAccountID mirror plugin.DefaultAccountID rather than repeat the literal. Left alone deliberately: the per-service jsonError/jsonResponse helpers, whose Content-Type differs by protocol (application/json vs x-amz-json-1.0 vs 1.1), and the randHex/generateID variants, which produce genuinely different id formats. Merge the two mirror-image case-conversion walkers in shared into one mapKeys(v, f) and move it to kafka, its only caller, with a test. --- internal/admin/api.go | 71 ------ internal/admin/api_test.go | 5 - .../templates/scaffold_provider.go.tmpl | 4 - internal/gateway/router_test.go | 3 - internal/plugin/plugin.go | 7 - internal/plugin/registry_test.go | 3 +- internal/services/account/provider.go | 91 ++++---- internal/services/acm/provider.go | 8 +- internal/services/acmpca/provider.go | 8 +- internal/services/amplify/provider.go | 27 +-- internal/services/apigatewayv2/provider.go | 35 ++- internal/services/appconfig/provider.go | 4 - .../applicationautoscaling/provider.go | 4 - internal/services/appsync/provider.go | 25 +-- internal/services/athena/provider.go | 117 +++++----- internal/services/autoscaling/provider.go | 4 - internal/services/backup/provider.go | 4 - internal/services/batch/provider.go | 71 +++--- internal/services/bedrock/provider.go | 29 +-- internal/services/cloudcontrol/provider.go | 4 - internal/services/cloudformation/provider.go | 4 - internal/services/cloudfront/provider.go | 4 - internal/services/cloudsearch/provider.go | 4 - internal/services/cloudtrail/provider.go | 4 - internal/services/cloudwatch/provider.go | 4 - internal/services/cloudwatchlogs/provider.go | 4 - internal/services/codeartifact/provider.go | 4 - internal/services/codebuild/provider.go | 4 - internal/services/codecommit/provider.go | 4 - internal/services/codeconnections/provider.go | 20 +- internal/services/codedeploy/provider.go | 4 - internal/services/codepipeline/provider.go | 4 - internal/services/cognitoidentity/provider.go | 8 +- .../cognitoidentityprovider/provider.go | 6 +- internal/services/configservice/provider.go | 101 ++++----- internal/services/configservice/store.go | 18 +- internal/services/costexplorer/provider.go | 4 - internal/services/dms/provider.go | 12 +- internal/services/docdb/provider.go | 4 - internal/services/dynamodb/provider.go | 8 - internal/services/dynamodbstreams/provider.go | 8 - internal/services/ebs/provider.go | 4 - internal/services/ec2/provider.go | 8 +- internal/services/ecr/provider.go | 75 +++---- internal/services/ecr/store.go | 2 +- internal/services/ecs/provider.go | 167 +++++++------- internal/services/ecs/store.go | 2 +- internal/services/efs/provider.go | 39 ++-- internal/services/eks/provider.go | 4 - internal/services/elasticache/provider.go | 4 - .../services/elasticbeanstalk/provider.go | 4 - .../elasticloadbalancingv2/provider.go | 4 - .../services/elasticsearchservice/provider.go | 4 - internal/services/emr/provider.go | 13 +- internal/services/eventbridge/provider.go | 4 - internal/services/eventbridge/store.go | 4 +- internal/services/firehose/provider.go | 4 - internal/services/fis/provider.go | 4 - internal/services/glacier/provider.go | 4 - internal/services/glue/provider.go | 185 ++++++++-------- internal/services/iam/provider.go | 5 - internal/services/iam/sts_provider.go | 5 - internal/services/identitystore/provider.go | 4 - internal/services/iot/provider.go | 13 +- internal/services/iotdataplane/provider.go | 4 - internal/services/iotwireless/provider.go | 33 ++- internal/services/kafka/provider.go | 67 +++++- internal/services/kafka/provider_test.go | 29 +++ internal/services/kinesis/provider.go | 4 - .../services/kinesisanalyticsv2/provider.go | 4 - internal/services/kms/provider.go | 8 +- internal/services/lakeformation/provider.go | 22 +- internal/services/lambda/provider.go | 13 +- .../services/managedblockchain/provider.go | 4 - internal/services/mediaconvert/provider.go | 5 - internal/services/mediaconvert/store.go | 4 +- internal/services/memorydb/provider.go | 4 - internal/services/mq/provider.go | 4 - internal/services/mwaa/provider.go | 4 - internal/services/neptune/provider.go | 4 - internal/services/opensearch/provider.go | 4 - internal/services/organizations/provider.go | 4 - internal/services/pinpoint/provider.go | 65 +++--- internal/services/pipes/provider.go | 4 - internal/services/ram/provider.go | 4 - internal/services/rds/provider.go | 4 - internal/services/redshift/provider.go | 4 - internal/services/resourcegroups/provider.go | 4 - .../resourcegroupstaggingapi/provider.go | 4 - internal/services/route53/provider.go | 4 - internal/services/route53resolver/provider.go | 157 +++++++------- internal/services/s3/provider.go | 5 - internal/services/s3tables/provider.go | 4 - internal/services/sagemaker/provider.go | 175 +++++++-------- internal/services/scheduler/provider.go | 4 - internal/services/secretsmanager/provider.go | 8 +- internal/services/serverlessrepo/provider.go | 4 - .../services/servicediscovery/provider.go | 4 - internal/services/ses/provider.go | 4 - internal/services/sesv2/provider.go | 4 - internal/services/sfn/provider.go | 4 - internal/services/shield/provider.go | 4 - internal/services/sns/provider.go | 8 +- internal/services/sqs/provider.go | 87 ++++---- internal/services/ssm/provider.go | 4 - internal/services/ssoadmin/provider.go | 4 - internal/services/support/provider.go | 4 - internal/services/swf/provider.go | 99 ++++----- internal/services/textract/provider.go | 4 - internal/services/timestreamwrite/provider.go | 4 - internal/services/transcribe/provider.go | 4 - internal/services/transfer/provider.go | 4 - .../services/verifiedpermissions/provider.go | 5 - internal/services/waf/provider.go | 179 +++++++-------- internal/services/wafv2/provider.go | 4 - internal/services/xray/provider.go | 4 - internal/shared/arn.go | 12 +- internal/shared/params.go | 11 + internal/shared/response.go | 93 -------- internal/shared/store.go | 114 ---------- internal/shared/store_test.go | 204 ------------------ 121 files changed, 944 insertions(+), 1914 deletions(-) create mode 100644 internal/shared/params.go delete mode 100644 internal/shared/store.go delete mode 100644 internal/shared/store_test.go diff --git a/internal/admin/api.go b/internal/admin/api.go index f00ce6be..9ab31dcd 100644 --- a/internal/admin/api.go +++ b/internal/admin/api.go @@ -32,8 +32,6 @@ func (d *API) Handler() http.Handler { mux.HandleFunc("/devcloud/api/services", d.handleServices) mux.HandleFunc("/devcloud/api/services/", d.handleServiceResources) - mux.HandleFunc("/devcloud/api/metrics", d.handleMetrics) - mux.HandleFunc("/devcloud/api/metrics/", d.handleServiceMetrics) mux.HandleFunc("/devcloud/api/logs", d.handleLogs) return mux @@ -73,10 +71,6 @@ func (d *API) handleServices(w http.ResponseWriter, r *http.Request) { } resources, _ := p.ListResources(ctx) - // GetMetrics is called but we only use the resource count from - // ListResources here; stats are available via /devcloud/api/metrics. - _, _ = p.GetMetrics(ctx) - services = append(services, serviceInfo{ ID: p.ServiceID(), Name: p.ServiceName(), @@ -118,71 +112,6 @@ func (d *API) handleServiceResources(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, resources) } -// aggregateMetrics is the payload for GET /devcloud/api/metrics. -type aggregateMetrics struct { - TotalRequests int64 `json:"totalRequests"` - ErrorCount int64 `json:"errorCount"` - Services int `json:"services"` -} - -// handleMetrics handles GET /devcloud/api/metrics. -func (d *API) handleMetrics(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - ids := d.registry.ActiveServices() - ctx := context.Background() - - var totalRequests, errorCount int64 - for _, id := range ids { - p, ok := d.registry.Get(id) - if !ok { - continue - } - m, err := p.GetMetrics(ctx) - if err != nil || m == nil { - continue - } - totalRequests += m.TotalRequests - errorCount += m.ErrorCount - } - - writeJSON(w, http.StatusOK, aggregateMetrics{ - TotalRequests: totalRequests, - ErrorCount: errorCount, - Services: len(ids), - }) -} - -// handleServiceMetrics handles GET /devcloud/api/metrics/{service}. -func (d *API) handleServiceMetrics(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - serviceID := strings.TrimPrefix(r.URL.Path, "/devcloud/api/metrics/") - - p, ok := d.registry.Get(serviceID) - if !ok { - http.Error(w, "service not found", http.StatusNotFound) - return - } - - m, err := p.GetMetrics(context.Background()) - if err != nil { - http.Error(w, "failed to get metrics", http.StatusInternalServerError) - return - } - if m == nil { - m = &plugin.ServiceMetrics{} - } - - writeJSON(w, http.StatusOK, m) -} - // handleLogs handles GET /devcloud/api/logs. func (d *API) handleLogs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { diff --git a/internal/admin/api_test.go b/internal/admin/api_test.go index 0990b51a..0ebd2ead 100644 --- a/internal/admin/api_test.go +++ b/internal/admin/api_test.go @@ -21,7 +21,6 @@ type mockServicePlugin struct { id string name string resources []plugin.Resource - metrics *plugin.ServiceMetrics } func (m *mockServicePlugin) ServiceID() string { return m.id } @@ -37,9 +36,6 @@ func (m *mockServicePlugin) HandleRequest(ctx context.Context, op string, req *h func (m *mockServicePlugin) ListResources(ctx context.Context) ([]plugin.Resource, error) { return m.resources, nil } -func (m *mockServicePlugin) GetMetrics(ctx context.Context) (*plugin.ServiceMetrics, error) { - return m.metrics, nil -} // newTestRegistry creates a Registry with a single mock plugin already active. func newTestRegistry(p *mockServicePlugin) *plugin.Registry { @@ -63,7 +59,6 @@ func TestAPI_Services(t *testing.T) { {Type: "bucket", ID: "my-bucket", Name: "my-bucket"}, {Type: "bucket", ID: "other-bucket", Name: "other-bucket"}, }, - metrics: &plugin.ServiceMetrics{TotalRequests: 10, ErrorCount: 1}, } reg := newTestRegistry(p) lc := NewLogCollector(10) diff --git a/internal/codegen/templates/scaffold_provider.go.tmpl b/internal/codegen/templates/scaffold_provider.go.tmpl index ea173100..3caceafa 100644 --- a/internal/codegen/templates/scaffold_provider.go.tmpl +++ b/internal/codegen/templates/scaffold_provider.go.tmpl @@ -35,10 +35,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return []plugin.Resource{}, nil } -func (p *Provider) GetMetrics(ctx context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func init() { plugin.DefaultRegistry.Register("{{ .ServiceID }}", func() plugin.ServicePlugin { return &Provider{} diff --git a/internal/gateway/router_test.go b/internal/gateway/router_test.go index 30ebdc12..fd28320d 100644 --- a/internal/gateway/router_test.go +++ b/internal/gateway/router_test.go @@ -28,9 +28,6 @@ func (s *stubPlugin) HandleRequest(_ context.Context, _ string, _ *http.Request) return s.response, nil } func (s *stubPlugin) ListResources(_ context.Context) ([]plugin.Resource, error) { return nil, nil } -func (s *stubPlugin) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} // registerStub injects a stub plugin directly into the registry's active map // by going through Init with a pre-registered factory. diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 3f1eda9e..017cf051 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -45,12 +45,6 @@ type Resource struct { Name string `json:"name"` } -type ServiceMetrics struct { - TotalRequests int64 `json:"total_requests"` - ErrorCount int64 `json:"error_count"` - ResourceCount int `json:"resource_count"` -} - type ServicePlugin interface { ServiceID() string ServiceName() string @@ -59,5 +53,4 @@ type ServicePlugin interface { Shutdown(ctx context.Context) error HandleRequest(ctx context.Context, op string, req *http.Request) (*Response, error) ListResources(ctx context.Context) ([]Resource, error) - GetMetrics(ctx context.Context) (*ServiceMetrics, error) } diff --git a/internal/plugin/registry_test.go b/internal/plugin/registry_test.go index 25bb3d54..d80091c3 100644 --- a/internal/plugin/registry_test.go +++ b/internal/plugin/registry_test.go @@ -21,8 +21,7 @@ func (m *mockPlugin) Shutdown(ctx context.Context) error { return nil } func (m *mockPlugin) HandleRequest(ctx context.Context, op string, req *http.Request) (*Response, error) { return &Response{StatusCode: 200, Body: []byte("ok")}, nil } -func (m *mockPlugin) ListResources(ctx context.Context) ([]Resource, error) { return nil, nil } -func (m *mockPlugin) GetMetrics(ctx context.Context) (*ServiceMetrics, error) { return nil, nil } +func (m *mockPlugin) ListResources(ctx context.Context) ([]Resource, error) { return nil, nil } func TestRegistryRegisterAndGet(t *testing.T) { reg := NewRegistry() diff --git a/internal/services/account/provider.go b/internal/services/account/provider.go index 72742ed6..516c8800 100644 --- a/internal/services/account/provider.go +++ b/internal/services/account/provider.go @@ -13,6 +13,8 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID @@ -74,7 +76,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. // --- Alternate Contact --- case "getalternatecontact": - contactType := strVal(bodyMap, "AlternateContactType") + contactType := shared.StrParam(bodyMap, "AlternateContactType") if contactType == "" { contactType = q.Get("AlternateContactType") } @@ -82,7 +84,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. case "putalternatecontact": return p.putAlternateContact(bodyMap) case "deletealternatecontact": - contactType := strVal(bodyMap, "AlternateContactType") + contactType := shared.StrParam(bodyMap, "AlternateContactType") if contactType == "" { contactType = q.Get("AlternateContactType") } @@ -92,7 +94,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. case "listregions": return p.listRegions() case "getregionoptstatus": - regionName := strVal(bodyMap, "RegionName") + regionName := shared.StrParam(bodyMap, "RegionName") if regionName == "" { regionName = q.Get("RegionName") } @@ -123,7 +125,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. case "alternatecontact": switch method { case http.MethodGet: - contactType := strVal(bodyMap, "AlternateContactType") + contactType := shared.StrParam(bodyMap, "AlternateContactType") if contactType == "" { contactType = q.Get("AlternateContactType") } @@ -131,7 +133,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. case http.MethodPut: return p.putAlternateContact(bodyMap) case http.MethodDelete: - contactType := strVal(bodyMap, "AlternateContactType") + contactType := shared.StrParam(bodyMap, "AlternateContactType") if contactType == "" { contactType = q.Get("AlternateContactType") } @@ -140,7 +142,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. case "regions": return p.listRegions() case "regionoptstatus": - regionName := strVal(bodyMap, "RegionName") + regionName := shared.StrParam(bodyMap, "RegionName") if regionName == "" { regionName = q.Get("RegionName") } @@ -162,7 +164,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. case "DeleteContactInformation": return p.deleteContactInformation() case "GetAlternateContact": - contactType := strVal(bodyMap, "AlternateContactType") + contactType := shared.StrParam(bodyMap, "AlternateContactType") if contactType == "" { contactType = q.Get("AlternateContactType") } @@ -170,7 +172,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. case "PutAlternateContact": return p.putAlternateContact(bodyMap) case "DeleteAlternateContact": - contactType := strVal(bodyMap, "AlternateContactType") + contactType := shared.StrParam(bodyMap, "AlternateContactType") if contactType == "" { contactType = q.Get("AlternateContactType") } @@ -178,7 +180,7 @@ func (p *AccountProvider) HandleRequest(_ context.Context, op string, req *http. case "ListRegions": return p.listRegions() case "GetRegionOptStatus": - regionName := strVal(bodyMap, "RegionName") + regionName := shared.StrParam(bodyMap, "RegionName") if regionName == "" { regionName = q.Get("RegionName") } @@ -208,10 +210,6 @@ func (p *AccountProvider) ListResources(_ context.Context) ([]plugin.Resource, e }, nil } -func (p *AccountProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func jsonError(code, message string, status int) *plugin.Response { @@ -235,15 +233,6 @@ func jsonResponse(status int, v any) (*plugin.Response, error) { }, nil } -func strVal(m map[string]any, key string) string { - if v, ok := m[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - // --- Contact Information --- func (p *AccountProvider) getContactInformation() (*plugin.Response, error) { @@ -271,24 +260,24 @@ func (p *AccountProvider) getContactInformation() (*plugin.Response, error) { func (p *AccountProvider) putContactInformation(body map[string]any) (*plugin.Response, error) { ci := &ContactInfo{} if contactRaw, ok := body["ContactInformation"].(map[string]any); ok { - ci.FullName = strVal(contactRaw, "FullName") - ci.CompanyName = strVal(contactRaw, "CompanyName") - ci.PhoneNumber = strVal(contactRaw, "PhoneNumber") - ci.AddressLine1 = strVal(contactRaw, "AddressLine1") - ci.City = strVal(contactRaw, "City") - ci.PostalCode = strVal(contactRaw, "PostalCode") - ci.CountryCode = strVal(contactRaw, "CountryCode") - ci.WebsiteURL = strVal(contactRaw, "WebsiteUrl") + ci.FullName = shared.StrParam(contactRaw, "FullName") + ci.CompanyName = shared.StrParam(contactRaw, "CompanyName") + ci.PhoneNumber = shared.StrParam(contactRaw, "PhoneNumber") + ci.AddressLine1 = shared.StrParam(contactRaw, "AddressLine1") + ci.City = shared.StrParam(contactRaw, "City") + ci.PostalCode = shared.StrParam(contactRaw, "PostalCode") + ci.CountryCode = shared.StrParam(contactRaw, "CountryCode") + ci.WebsiteURL = shared.StrParam(contactRaw, "WebsiteUrl") } else { // Flat body - ci.FullName = strVal(body, "FullName") - ci.CompanyName = strVal(body, "CompanyName") - ci.PhoneNumber = strVal(body, "PhoneNumber") - ci.AddressLine1 = strVal(body, "AddressLine1") - ci.City = strVal(body, "City") - ci.PostalCode = strVal(body, "PostalCode") - ci.CountryCode = strVal(body, "CountryCode") - ci.WebsiteURL = strVal(body, "WebsiteUrl") + ci.FullName = shared.StrParam(body, "FullName") + ci.CompanyName = shared.StrParam(body, "CompanyName") + ci.PhoneNumber = shared.StrParam(body, "PhoneNumber") + ci.AddressLine1 = shared.StrParam(body, "AddressLine1") + ci.City = shared.StrParam(body, "City") + ci.PostalCode = shared.StrParam(body, "PostalCode") + ci.CountryCode = shared.StrParam(body, "CountryCode") + ci.WebsiteURL = shared.StrParam(body, "WebsiteUrl") } if err := p.store.PutContactInfo(defaultAccountID, ci); err != nil { return jsonError("InternalError", err.Error(), http.StatusInternalServerError), nil @@ -327,17 +316,17 @@ func (p *AccountProvider) getAlternateContact(contactType string) (*plugin.Respo func (p *AccountProvider) putAlternateContact(body map[string]any) (*plugin.Response, error) { c := &AlternateContact{} if acRaw, ok := body["AlternateContact"].(map[string]any); ok { - c.ContactType = strVal(acRaw, "AlternateContactType") - c.Name = strVal(acRaw, "Name") - c.Title = strVal(acRaw, "Title") - c.Email = strVal(acRaw, "EmailAddress") - c.PhoneNumber = strVal(acRaw, "PhoneNumber") + c.ContactType = shared.StrParam(acRaw, "AlternateContactType") + c.Name = shared.StrParam(acRaw, "Name") + c.Title = shared.StrParam(acRaw, "Title") + c.Email = shared.StrParam(acRaw, "EmailAddress") + c.PhoneNumber = shared.StrParam(acRaw, "PhoneNumber") } else { - c.ContactType = strVal(body, "AlternateContactType") - c.Name = strVal(body, "Name") - c.Title = strVal(body, "Title") - c.Email = strVal(body, "EmailAddress") - c.PhoneNumber = strVal(body, "PhoneNumber") + c.ContactType = shared.StrParam(body, "AlternateContactType") + c.Name = shared.StrParam(body, "Name") + c.Title = shared.StrParam(body, "Title") + c.Email = shared.StrParam(body, "EmailAddress") + c.PhoneNumber = shared.StrParam(body, "PhoneNumber") } if err := p.store.PutAlternateContact(defaultAccountID, c); err != nil { return jsonError("InternalError", err.Error(), http.StatusInternalServerError), nil @@ -384,7 +373,7 @@ func (p *AccountProvider) getRegionOptStatus(regionName string) (*plugin.Respons } func (p *AccountProvider) enableRegion(body map[string]any) (*plugin.Response, error) { - regionName := strVal(body, "RegionName") + regionName := shared.StrParam(body, "RegionName") if err := p.store.SetRegionOptStatus(defaultAccountID, regionName, "ENABLED"); err != nil { return jsonError("InternalError", err.Error(), http.StatusInternalServerError), nil } @@ -392,7 +381,7 @@ func (p *AccountProvider) enableRegion(body map[string]any) (*plugin.Response, e } func (p *AccountProvider) disableRegion(body map[string]any) (*plugin.Response, error) { - regionName := strVal(body, "RegionName") + regionName := shared.StrParam(body, "RegionName") if err := p.store.SetRegionOptStatus(defaultAccountID, regionName, "DISABLED"); err != nil { return jsonError("InternalError", err.Error(), http.StatusInternalServerError), nil } @@ -410,7 +399,7 @@ func (p *AccountProvider) getPrimaryEmail() (*plugin.Response, error) { } func (p *AccountProvider) startPrimaryEmailUpdate(body map[string]any) (*plugin.Response, error) { - pendingEmail := strVal(body, "PrimaryEmail") + pendingEmail := shared.StrParam(body, "PrimaryEmail") if err := p.store.StartPrimaryEmailUpdate(defaultAccountID, pendingEmail); err != nil { return jsonError("InternalError", err.Error(), http.StatusInternalServerError), nil } diff --git a/internal/services/acm/provider.go b/internal/services/acm/provider.go index 9226d0dd..e0997395 100644 --- a/internal/services/acm/provider.go +++ b/internal/services/acm/provider.go @@ -20,10 +20,12 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID -const defaultRegion = "us-east-1" +const defaultRegion = shared.DefaultRegion // Provider implements ACM using the JSON 1.1 protocol. // Operations are dispatched via X-Amz-Target header (value passed as op). @@ -150,10 +152,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func arnForCert(accountID, region, id string) string { diff --git a/internal/services/acmpca/provider.go b/internal/services/acmpca/provider.go index 3cbbc189..4598387f 100644 --- a/internal/services/acmpca/provider.go +++ b/internal/services/acmpca/provider.go @@ -19,10 +19,12 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID -const defaultRegion = "us-east-1" +const defaultRegion = shared.DefaultRegion // Provider implements the ACMPrivateCA service using JSON 1.1 protocol. type Provider struct { @@ -120,10 +122,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func newID() string { diff --git a/internal/services/amplify/provider.go b/internal/services/amplify/provider.go index 5f898260..248c4fdf 100644 --- a/internal/services/amplify/provider.go +++ b/internal/services/amplify/provider.go @@ -403,10 +403,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- App CRUD --- func (p *Provider) createApp(params map[string]any) (*plugin.Response, error) { @@ -422,10 +418,10 @@ func (p *Provider) createApp(params map[string]any) (*plugin.Response, error) { ID: id, ARN: arn, Name: name, - Description: strParam(params, "description"), - Repository: strParam(params, "repository"), + Description: shared.StrParam(params, "description"), + Repository: shared.StrParam(params, "repository"), Platform: strParamDefault(params, "platform", "WEB"), - IAMRole: strParam(params, "iamServiceRoleArn"), + IAMRole: shared.StrParam(params, "iamServiceRoleArn"), DefaultDomain: defaultDomain, } @@ -510,9 +506,9 @@ func (p *Provider) createBranch(appID string, params map[string]any) (*plugin.Re Name: branchName, ARN: arn, DisplayName: strParamDefault(params, "displayName", branchName), - Description: strParam(params, "description"), + Description: shared.StrParam(params, "description"), Stage: strParamDefault(params, "stage", "NONE"), - Framework: strParam(params, "framework"), + Framework: shared.StrParam(params, "framework"), EnableAutoBuild: boolParamDefault(params, "enableAutoBuild", true), } if err := p.store.CreateBranch(b); err != nil { @@ -658,7 +654,7 @@ func (p *Provider) createWebhook(appID string, params map[string]any) (*plugin.R ID: id, ARN: arn, AppID: appID, - BranchName: strParam(params, "branchName"), + BranchName: shared.StrParam(params, "branchName"), URL: fmt.Sprintf("https://webhooks.amplify.us-east-1.io/%s", id), } if err := p.store.CreateWebhook(w); err != nil { @@ -727,7 +723,7 @@ func (p *Provider) createBackendEnvironment(appID string, params map[string]any) AppID: appID, Name: envName, ARN: arn, - StackName: strParam(params, "stackName"), + StackName: shared.StrParam(params, "stackName"), } if err := p.store.CreateBackendEnvironment(be); err != nil { if isUniqueErr(err) { @@ -787,8 +783,8 @@ func (p *Provider) startJob(appID, branchName string, params map[string]any) (*p ARN: arn, JobType: strParamDefault(params, "jobType", "RELEASE"), Status: "SUCCEED", - CommitID: strParam(params, "commitId"), - CommitMsg: strParam(params, "commitMessage"), + CommitID: shared.StrParam(params, "commitId"), + CommitMsg: shared.StrParam(params, "commitMessage"), } if err := p.store.CreateJob(j); err != nil { return nil, err @@ -988,11 +984,6 @@ func extractPathParam(path, key string) string { return "" } -func strParam(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func strParamDefault(params map[string]any, key, def string) string { if v, ok := params[key].(string); ok && v != "" { return v diff --git a/internal/services/apigatewayv2/provider.go b/internal/services/apigatewayv2/provider.go index 7c0d2955..80b16bc5 100644 --- a/internal/services/apigatewayv2/provider.go +++ b/internal/services/apigatewayv2/provider.go @@ -341,10 +341,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- API CRUD --- func (p *Provider) createAPI(params map[string]any) (*plugin.Response, error) { @@ -361,7 +357,7 @@ func (p *Provider) createAPI(params map[string]any) (*plugin.Response, error) { ARN: arn, Name: name, ProtocolType: strParamDefault(params, "protocolType", "HTTP"), - Description: strParam(params, "description"), + Description: shared.StrParam(params, "description"), RouteSelection: strParamDefault(params, "routeSelectionExpression", "$request.method $request.path"), APIEndpoint: apiEndpoint, } @@ -479,9 +475,9 @@ func (p *Provider) createRoute(apiID string, params map[string]any) (*plugin.Res ID: shared.GenerateID("", 10), APIID: apiID, RouteKey: routeKey, - Target: strParam(params, "target"), + Target: shared.StrParam(params, "target"), AuthorizationType: strParamDefault(params, "authorizationType", "NONE"), - AuthorizerID: strParam(params, "authorizerId"), + AuthorizerID: shared.StrParam(params, "authorizerId"), } if err := p.store.CreateRoute(r); err != nil { return nil, err @@ -544,7 +540,7 @@ func (p *Provider) createRouteResponse(apiID, routeID string, params map[string] APIID: apiID, RouteID: routeID, RouteResponseKey: strParamDefault(params, "routeResponseKey", "default"), - ModelSelectionExpr: strParam(params, "modelSelectionExpression"), + ModelSelectionExpr: shared.StrParam(params, "modelSelectionExpression"), } if err := p.store.CreateRouteResponse(rr); err != nil { return nil, err @@ -589,8 +585,8 @@ func (p *Provider) createIntegration(apiID string, params map[string]any) (*plug ID: shared.GenerateID("", 10), APIID: apiID, Type: strParamDefault(params, "integrationType", "HTTP_PROXY"), - IntegrationURI: strParam(params, "integrationUri"), - IntegrationMethod: strParam(params, "integrationMethod"), + IntegrationURI: shared.StrParam(params, "integrationUri"), + IntegrationMethod: shared.StrParam(params, "integrationMethod"), PayloadFormat: strParamDefault(params, "payloadFormatVersion", "2.0"), } if err := p.store.CreateIntegration(i); err != nil { @@ -642,7 +638,7 @@ func (p *Provider) createIntegrationResponse(apiID, integrationID string, params APIID: apiID, IntegrationID: integrationID, ResponseKey: strParamDefault(params, "integrationResponseKey", "default"), - TemplateSelection: strParam(params, "templateSelectionExpression"), + TemplateSelection: shared.StrParam(params, "templateSelectionExpression"), } if err := p.store.CreateIntegrationResponse(ir); err != nil { return nil, err @@ -706,7 +702,7 @@ func (p *Provider) createAuthorizer(apiID string, params map[string]any) (*plugi APIID: apiID, Name: name, Type: strParamDefault(params, "authorizerType", "JWT"), - IdentitySource: strParam(params, "identitySource"), + IdentitySource: shared.StrParam(params, "identitySource"), JWTConfig: jwtConfig, } if err := p.store.CreateAuthorizer(a); err != nil { @@ -759,7 +755,7 @@ func (p *Provider) createDeployment(apiID string, params map[string]any) (*plugi d := &Deployment{ ID: shared.GenerateID("", 10), APIID: apiID, - Description: strParam(params, "description"), + Description: shared.StrParam(params, "description"), } if err := p.store.CreateDeployment(d); err != nil { return nil, err @@ -821,8 +817,8 @@ func (p *Provider) createStage(apiID string, params map[string]any) (*plugin.Res st := &Stage{ Name: stageName, APIID: apiID, - Description: strParam(params, "description"), - DeploymentID: strParam(params, "deploymentId"), + Description: shared.StrParam(params, "description"), + DeploymentID: shared.StrParam(params, "deploymentId"), AutoDeploy: boolParamDefault(params, "autoDeploy", false), StageVariables: stageVars, AccessLog: "{}", @@ -1013,8 +1009,8 @@ func (p *Provider) createAPIMapping(domainName string, params map[string]any) (* ID: shared.GenerateID("", 10), APIID: apiID, DomainName: domainName, - Stage: strParam(params, "stage"), - APIMappingKey: strParam(params, "apiMappingKey"), + Stage: shared.StrParam(params, "stage"), + APIMappingKey: shared.StrParam(params, "apiMappingKey"), } if err := p.store.CreateAPIMapping(m); err != nil { return nil, err @@ -1610,11 +1606,6 @@ func extractPathParam(path, key string) string { return "" } -func strParam(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func strParamDefault(params map[string]any, key, def string) string { if v, ok := params[key].(string); ok && v != "" { return v diff --git a/internal/services/appconfig/provider.go b/internal/services/appconfig/provider.go index 6df70a4e..94c13f18 100644 --- a/internal/services/appconfig/provider.go +++ b/internal/services/appconfig/provider.go @@ -224,10 +224,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Applications --- func (p *Provider) createApplication(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/applicationautoscaling/provider.go b/internal/services/applicationautoscaling/provider.go index 59d04d1d..dfe2c845 100644 --- a/internal/services/applicationautoscaling/provider.go +++ b/internal/services/applicationautoscaling/provider.go @@ -167,10 +167,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) registerScalableTarget(params map[string]any) (*plugin.Response, error) { ns, _ := params["ServiceNamespace"].(string) resourceID, _ := params["ResourceId"].(string) diff --git a/internal/services/appsync/provider.go b/internal/services/appsync/provider.go index 907cc855..2c65a67a 100644 --- a/internal/services/appsync/provider.go +++ b/internal/services/appsync/provider.go @@ -439,10 +439,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- GraphqlApi CRUD --- func (p *Provider) createGraphqlApi(params map[string]any) (*plugin.Response, error) { @@ -544,7 +540,7 @@ func (p *Provider) createDataSource(apiID string, params map[string]any) (*plugi ARN: arn, Type: strParamDefault(params, "type", "NONE"), Config: "{}", - ServiceRole: strParam(params, "serviceRoleArn"), + ServiceRole: shared.StrParam(params, "serviceRoleArn"), } if err := p.store.CreateDataSource(ds); err != nil { if isUniqueErr(err) { @@ -616,9 +612,9 @@ func (p *Provider) createResolver(apiID, typeName string, params map[string]any) TypeName: typeName, FieldName: fieldName, ARN: arn, - DataSource: strParam(params, "dataSourceName"), - RequestTemplate: strParam(params, "requestMappingTemplate"), - ResponseTemplate: strParam(params, "responseMappingTemplate"), + DataSource: shared.StrParam(params, "dataSourceName"), + RequestTemplate: shared.StrParam(params, "requestMappingTemplate"), + ResponseTemplate: shared.StrParam(params, "responseMappingTemplate"), Kind: strParamDefault(params, "kind", "UNIT"), } if err := p.store.CreateResolver(r); err != nil { @@ -704,9 +700,9 @@ func (p *Provider) createFunction(apiID string, params map[string]any) (*plugin. ID: id, ARN: arn, Name: name, - DataSource: strParam(params, "dataSourceName"), - RequestTemplate: strParam(params, "requestMappingTemplate"), - ResponseTemplate: strParam(params, "responseMappingTemplate"), + DataSource: shared.StrParam(params, "dataSourceName"), + RequestTemplate: shared.StrParam(params, "requestMappingTemplate"), + ResponseTemplate: shared.StrParam(params, "responseMappingTemplate"), } if err := p.store.CreateFunction(f); err != nil { return nil, err @@ -774,7 +770,7 @@ func (p *Provider) createApiKey(apiID string, params map[string]any) (*plugin.Re ApiID: apiID, ID: id, Expires: expires, - Description: strParam(params, "description"), + Description: shared.StrParam(params, "description"), } if err := p.store.CreateApiKey(k); err != nil { return nil, err @@ -1026,11 +1022,6 @@ func extractTagARN(path string) string { return path[idx+len("/tags/"):] } -func strParam(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func strParamDefault(params map[string]any, key, def string) string { if v, ok := params[key].(string); ok && v != "" { return v diff --git a/internal/services/athena/provider.go b/internal/services/athena/provider.go index b45e9cec..3a7041ef 100644 --- a/internal/services/athena/provider.go +++ b/internal/services/athena/provider.go @@ -137,14 +137,14 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{}) case "GetCapacityReservation": return shared.JSONResponse(http.StatusOK, map[string]any{ - "CapacityReservation": map[string]any{"Name": strParam(params, "Name"), "Status": "ACTIVE", "TargetDpus": 24}, + "CapacityReservation": map[string]any{"Name": shared.StrParam(params, "Name"), "Status": "ACTIVE", "TargetDpus": 24}, }) case "ListCapacityReservations": return shared.JSONResponse(http.StatusOK, map[string]any{"CapacityReservations": []any{}}) case "GetCapacityAssignmentConfiguration": return shared.JSONResponse(http.StatusOK, map[string]any{ "CapacityAssignmentConfiguration": map[string]any{ - "CapacityReservationName": strParam(params, "CapacityReservationName"), + "CapacityReservationName": shared.StrParam(params, "CapacityReservationName"), "CapacityAssignments": []any{}, }, }) @@ -153,7 +153,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ExportNotebook": return shared.JSONResponse(http.StatusOK, map[string]any{"NotebookMetadata": map[string]any{}, "Payload": ""}) case "GetNotebookMetadata": - return shared.JSONResponse(http.StatusOK, map[string]any{"NotebookMetadata": map[string]any{"NotebookId": strParam(params, "NotebookId")}}) + return shared.JSONResponse(http.StatusOK, map[string]any{"NotebookMetadata": map[string]any{"NotebookId": shared.StrParam(params, "NotebookId")}}) case "ListNotebookMetadata": return shared.JSONResponse(http.StatusOK, map[string]any{"NotebookMetadataList": []any{}}) case "CreatePresignedNotebookUrl": @@ -163,9 +163,9 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "StartSession": return shared.JSONResponse(http.StatusOK, map[string]any{"SessionId": shared.GenerateUUID(), "State": "CREATING"}) case "GetSession": - return shared.JSONResponse(http.StatusOK, map[string]any{"SessionId": strParam(params, "SessionId"), "Status": map[string]any{"State": "IDLE"}}) + return shared.JSONResponse(http.StatusOK, map[string]any{"SessionId": shared.StrParam(params, "SessionId"), "Status": map[string]any{"State": "IDLE"}}) case "GetSessionStatus": - return shared.JSONResponse(http.StatusOK, map[string]any{"SessionId": strParam(params, "SessionId"), "Status": map[string]any{"State": "IDLE"}}) + return shared.JSONResponse(http.StatusOK, map[string]any{"SessionId": shared.StrParam(params, "SessionId"), "Status": map[string]any{"State": "IDLE"}}) case "GetSessionEndpoint": return shared.JSONResponse(http.StatusOK, map[string]any{"Endpoint": ""}) case "ListSessions": @@ -177,7 +177,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "StopCalculationExecution": return shared.JSONResponse(http.StatusOK, map[string]any{"State": "CANCELING"}) case "GetCalculationExecution": - return shared.JSONResponse(http.StatusOK, map[string]any{"CalculationExecutionId": strParam(params, "CalculationExecutionId"), "Status": map[string]any{"State": "COMPLETED"}}) + return shared.JSONResponse(http.StatusOK, map[string]any{"CalculationExecutionId": shared.StrParam(params, "CalculationExecutionId"), "Status": map[string]any{"State": "COMPLETED"}}) case "GetCalculationExecutionCode": return shared.JSONResponse(http.StatusOK, map[string]any{"CodeBlock": ""}) case "GetCalculationExecutionStatus": @@ -188,13 +188,13 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"Executors": []any{}}) case "GetDatabase": return shared.JSONResponse(http.StatusOK, map[string]any{ - "Database": map[string]any{"Name": strParam(params, "Database"), "Description": ""}, + "Database": map[string]any{"Name": shared.StrParam(params, "Database"), "Description": ""}, }) case "ListDatabases": return shared.JSONResponse(http.StatusOK, map[string]any{"DatabaseList": []any{}}) case "GetTableMetadata": return shared.JSONResponse(http.StatusOK, map[string]any{ - "TableMetadata": map[string]any{"Name": strParam(params, "TableName"), "Columns": []any{}}, + "TableMetadata": map[string]any{"Name": shared.StrParam(params, "TableName"), "Columns": []any{}}, }) case "ListTableMetadata": return shared.JSONResponse(http.StatusOK, map[string]any{"TableMetadataList": []any{}}) @@ -224,18 +224,14 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- WorkGroup handlers ---- func (p *Provider) createWorkGroup(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidRequestException", "Name is required", http.StatusBadRequest), nil } - description := strParam(params, "Description") + description := shared.StrParam(params, "Description") configJSON := "{}" if cfg, ok := params["Configuration"]; ok { b, _ := json.Marshal(cfg) @@ -257,7 +253,7 @@ func (p *Provider) createWorkGroup(params map[string]any) (*plugin.Response, err } func (p *Provider) getWorkGroup(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "WorkGroup") + name := shared.StrParam(params, "WorkGroup") if name == "" { return shared.JSONError("InvalidRequestException", "WorkGroup is required", http.StatusBadRequest), nil } @@ -288,7 +284,7 @@ func (p *Provider) listWorkGroups(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) deleteWorkGroup(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "WorkGroup") + name := shared.StrParam(params, "WorkGroup") if name == "" { return shared.JSONError("InvalidRequestException", "WorkGroup is required", http.StatusBadRequest), nil } @@ -304,7 +300,7 @@ func (p *Provider) deleteWorkGroup(params map[string]any) (*plugin.Response, err } func (p *Provider) updateWorkGroup(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "WorkGroup") + name := shared.StrParam(params, "WorkGroup") if name == "" { return shared.JSONError("InvalidRequestException", "WorkGroup is required", http.StatusBadRequest), nil } @@ -312,11 +308,11 @@ func (p *Provider) updateWorkGroup(params map[string]any) (*plugin.Response, err if err != nil { return shared.JSONError("InvalidRequestException", "WorkGroup not found", http.StatusBadRequest), nil } - description := strParam(params, "Description") + description := shared.StrParam(params, "Description") if description == "" { description = wg.Description } - state := strParam(params, "State") + state := shared.StrParam(params, "State") if state == "" { state = wg.State } @@ -346,17 +342,17 @@ func workGroupToMap(wg *WorkGroup) map[string]any { // ---- NamedQuery handlers ---- func (p *Provider) createNamedQuery(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") - queryString := strParam(params, "QueryString") + name := shared.StrParam(params, "Name") + queryString := shared.StrParam(params, "QueryString") if name == "" || queryString == "" { return shared.JSONError("InvalidRequestException", "Name and QueryString are required", http.StatusBadRequest), nil } - workgroup := strParam(params, "WorkGroup") + workgroup := shared.StrParam(params, "WorkGroup") if workgroup == "" { workgroup = "primary" } - database := strParam(params, "Database") - description := strParam(params, "Description") + database := shared.StrParam(params, "Database") + description := shared.StrParam(params, "Description") id := shared.GenerateUUID() nq, err := p.store.CreateNamedQuery(id, name, workgroup, database, queryString, description) if err != nil { @@ -366,7 +362,7 @@ func (p *Provider) createNamedQuery(params map[string]any) (*plugin.Response, er } func (p *Provider) getNamedQuery(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "NamedQueryId") + id := shared.StrParam(params, "NamedQueryId") if id == "" { return shared.JSONError("InvalidRequestException", "NamedQueryId is required", http.StatusBadRequest), nil } @@ -380,7 +376,7 @@ func (p *Provider) getNamedQuery(params map[string]any) (*plugin.Response, error } func (p *Provider) listNamedQueries(params map[string]any) (*plugin.Response, error) { - workgroup := strParam(params, "WorkGroup") + workgroup := shared.StrParam(params, "WorkGroup") queries, err := p.store.ListNamedQueries(workgroup) if err != nil { return nil, err @@ -393,7 +389,7 @@ func (p *Provider) listNamedQueries(params map[string]any) (*plugin.Response, er } func (p *Provider) deleteNamedQuery(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "NamedQueryId") + id := shared.StrParam(params, "NamedQueryId") if id == "" { return shared.JSONError("InvalidRequestException", "NamedQueryId is required", http.StatusBadRequest), nil } @@ -436,11 +432,11 @@ func namedQueryToMap(nq *NamedQuery) map[string]any { // ---- QueryExecution handlers ---- func (p *Provider) startQueryExecution(params map[string]any) (*plugin.Response, error) { - query := strParam(params, "QueryString") + query := shared.StrParam(params, "QueryString") if query == "" { return shared.JSONError("InvalidRequestException", "QueryString is required", http.StatusBadRequest), nil } - workgroup := strParam(params, "WorkGroup") + workgroup := shared.StrParam(params, "WorkGroup") if workgroup == "" { workgroup = "primary" } @@ -457,7 +453,7 @@ func (p *Provider) startQueryExecution(params map[string]any) (*plugin.Response, } func (p *Provider) getQueryExecution(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "QueryExecutionId") + id := shared.StrParam(params, "QueryExecutionId") if id == "" { return shared.JSONError("InvalidRequestException", "QueryExecutionId is required", http.StatusBadRequest), nil } @@ -471,7 +467,7 @@ func (p *Provider) getQueryExecution(params map[string]any) (*plugin.Response, e } func (p *Provider) getQueryResults(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "QueryExecutionId") + id := shared.StrParam(params, "QueryExecutionId") if id == "" { return shared.JSONError("InvalidRequestException", "QueryExecutionId is required", http.StatusBadRequest), nil } @@ -492,7 +488,7 @@ func (p *Provider) getQueryResults(params map[string]any) (*plugin.Response, err } func (p *Provider) listQueryExecutions(params map[string]any) (*plugin.Response, error) { - workgroup := strParam(params, "WorkGroup") + workgroup := shared.StrParam(params, "WorkGroup") execs, err := p.store.ListQueryExecutions(workgroup) if err != nil { return nil, err @@ -505,7 +501,7 @@ func (p *Provider) listQueryExecutions(params map[string]any) (*plugin.Response, } func (p *Provider) stopQueryExecution(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "QueryExecutionId") + id := shared.StrParam(params, "QueryExecutionId") if id == "" { return shared.JSONError("InvalidRequestException", "QueryExecutionId is required", http.StatusBadRequest), nil } @@ -535,7 +531,7 @@ func (p *Provider) batchGetQueryExecution(params map[string]any) (*plugin.Respon } func (p *Provider) getQueryRuntimeStatistics(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "QueryExecutionId") + id := shared.StrParam(params, "QueryExecutionId") if id == "" { return shared.JSONError("InvalidRequestException", "QueryExecutionId is required", http.StatusBadRequest), nil } @@ -577,15 +573,15 @@ func queryExecutionToMap(qe *QueryExecution) map[string]any { // ---- DataCatalog handlers ---- func (p *Provider) createDataCatalog(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidRequestException", "Name is required", http.StatusBadRequest), nil } - catalogType := strParam(params, "Type") + catalogType := shared.StrParam(params, "Type") if catalogType == "" { catalogType = "HIVE" } - description := strParam(params, "Description") + description := shared.StrParam(params, "Description") parametersJSON := "{}" if p2, ok := params["Parameters"]; ok { b, _ := json.Marshal(p2) @@ -602,7 +598,7 @@ func (p *Provider) createDataCatalog(params map[string]any) (*plugin.Response, e } func (p *Provider) getDataCatalog(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidRequestException", "Name is required", http.StatusBadRequest), nil } @@ -631,7 +627,7 @@ func (p *Provider) listDataCatalogs(_ map[string]any) (*plugin.Response, error) } func (p *Provider) updateDataCatalog(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidRequestException", "Name is required", http.StatusBadRequest), nil } @@ -639,7 +635,7 @@ func (p *Provider) updateDataCatalog(params map[string]any) (*plugin.Response, e if err != nil { return shared.JSONError("InvalidRequestException", "DataCatalog not found", http.StatusBadRequest), nil } - description := strParam(params, "Description") + description := shared.StrParam(params, "Description") if description == "" { description = dc.Description } @@ -655,7 +651,7 @@ func (p *Provider) updateDataCatalog(params map[string]any) (*plugin.Response, e } func (p *Provider) deleteDataCatalog(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidRequestException", "Name is required", http.StatusBadRequest), nil } @@ -679,13 +675,13 @@ func dataCatalogToMap(dc *DataCatalog) map[string]any { // ---- PreparedStatement handlers ---- func (p *Provider) createPreparedStatement(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "StatementName") - workgroup := strParam(params, "WorkGroup") - queryStatement := strParam(params, "QueryStatement") + name := shared.StrParam(params, "StatementName") + workgroup := shared.StrParam(params, "WorkGroup") + queryStatement := shared.StrParam(params, "QueryStatement") if name == "" || workgroup == "" || queryStatement == "" { return shared.JSONError("InvalidRequestException", "StatementName, WorkGroup, and QueryStatement are required", http.StatusBadRequest), nil } - description := strParam(params, "Description") + description := shared.StrParam(params, "Description") if _, err := p.store.CreatePreparedStatement(name, workgroup, queryStatement, description); err != nil { if sqliteIsUnique(err) { return shared.JSONError("InvalidRequestException", "PreparedStatement already exists", http.StatusBadRequest), nil @@ -696,8 +692,8 @@ func (p *Provider) createPreparedStatement(params map[string]any) (*plugin.Respo } func (p *Provider) getPreparedStatement(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "StatementName") - workgroup := strParam(params, "WorkGroup") + name := shared.StrParam(params, "StatementName") + workgroup := shared.StrParam(params, "WorkGroup") if name == "" || workgroup == "" { return shared.JSONError("InvalidRequestException", "StatementName and WorkGroup are required", http.StatusBadRequest), nil } @@ -711,7 +707,7 @@ func (p *Provider) getPreparedStatement(params map[string]any) (*plugin.Response } func (p *Provider) listPreparedStatements(params map[string]any) (*plugin.Response, error) { - workgroup := strParam(params, "WorkGroup") + workgroup := shared.StrParam(params, "WorkGroup") if workgroup == "" { return shared.JSONError("InvalidRequestException", "WorkGroup is required", http.StatusBadRequest), nil } @@ -730,13 +726,13 @@ func (p *Provider) listPreparedStatements(params map[string]any) (*plugin.Respon } func (p *Provider) updatePreparedStatement(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "StatementName") - workgroup := strParam(params, "WorkGroup") - queryStatement := strParam(params, "QueryStatement") + name := shared.StrParam(params, "StatementName") + workgroup := shared.StrParam(params, "WorkGroup") + queryStatement := shared.StrParam(params, "QueryStatement") if name == "" || workgroup == "" || queryStatement == "" { return shared.JSONError("InvalidRequestException", "StatementName, WorkGroup, and QueryStatement are required", http.StatusBadRequest), nil } - description := strParam(params, "Description") + description := shared.StrParam(params, "Description") if err := p.store.UpdatePreparedStatement(name, workgroup, queryStatement, description); err != nil { return shared.JSONError("InvalidRequestException", "PreparedStatement not found", http.StatusBadRequest), nil } @@ -744,8 +740,8 @@ func (p *Provider) updatePreparedStatement(params map[string]any) (*plugin.Respo } func (p *Provider) deletePreparedStatement(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "StatementName") - workgroup := strParam(params, "WorkGroup") + name := shared.StrParam(params, "StatementName") + workgroup := shared.StrParam(params, "WorkGroup") if name == "" || workgroup == "" { return shared.JSONError("InvalidRequestException", "StatementName and WorkGroup are required", http.StatusBadRequest), nil } @@ -756,7 +752,7 @@ func (p *Provider) deletePreparedStatement(params map[string]any) (*plugin.Respo } func (p *Provider) batchGetPreparedStatement(params map[string]any) (*plugin.Response, error) { - workgroup := strParam(params, "WorkGroup") + workgroup := shared.StrParam(params, "WorkGroup") rawNames, _ := params["PreparedStatementNames"].([]any) var found []map[string]any var unprocessed []map[string]any @@ -788,7 +784,7 @@ func preparedStatementToMap(ps *PreparedStatement) map[string]any { // ---- Tag handlers ---- func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceARN") + arn := shared.StrParam(params, "ResourceARN") if arn == "" { return shared.JSONError("InvalidRequestException", "ResourceARN is required", http.StatusBadRequest), nil } @@ -800,7 +796,7 @@ func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) } func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceARN") + arn := shared.StrParam(params, "ResourceARN") if arn == "" { return shared.JSONError("InvalidRequestException", "ResourceARN is required", http.StatusBadRequest), nil } @@ -818,7 +814,7 @@ func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error } func (p *Provider) listTagsForResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceARN") + arn := shared.StrParam(params, "ResourceARN") if arn == "" { return shared.JSONError("InvalidRequestException", "ResourceARN is required", http.StatusBadRequest), nil } @@ -835,11 +831,6 @@ func (p *Provider) listTagsForResource(params map[string]any) (*plugin.Response, // ---- helpers ---- -func strParam(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func parseTags(rawTags []any) map[string]string { tags := make(map[string]string) for _, t := range rawTags { diff --git a/internal/services/autoscaling/provider.go b/internal/services/autoscaling/provider.go index 4f24678d..2bd4f229 100644 --- a/internal/services/autoscaling/provider.go +++ b/internal/services/autoscaling/provider.go @@ -193,10 +193,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func asError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/backup/provider.go b/internal/services/backup/provider.go index 2a7e0021..0483fbee 100644 --- a/internal/services/backup/provider.go +++ b/internal/services/backup/provider.go @@ -331,10 +331,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ========== BackupPlan ========== func (p *Provider) createBackupPlan(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/batch/provider.go b/internal/services/batch/provider.go index 4ff8a017..9a5c2e5b 100644 --- a/internal/services/batch/provider.go +++ b/internal/services/batch/provider.go @@ -134,8 +134,8 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // Stub operations — ConsumableResource, QuotaShare, ServiceEnvironment, ServiceJob case "CreateConsumableResource": return shared.JSONResponse(http.StatusOK, map[string]any{ - "consumableResourceArn": shared.BuildARN("batch", "consumable-resource", strParam(params, "consumableResourceName")), - "consumableResourceName": strParam(params, "consumableResourceName"), + "consumableResourceArn": shared.BuildARN("batch", "consumable-resource", shared.StrParam(params, "consumableResourceName")), + "consumableResourceName": shared.StrParam(params, "consumableResourceName"), }) case "DeleteConsumableResource": return shared.JSONResponse(http.StatusOK, map[string]any{}) @@ -162,8 +162,8 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"jobs": []any{}, "nextToken": ""}) case "CreateQuotaShare": return shared.JSONResponse(http.StatusOK, map[string]any{ - "quotaShareArn": shared.BuildARN("batch", "quota-share", strParam(params, "quotaShareName")), - "quotaShareName": strParam(params, "quotaShareName"), + "quotaShareArn": shared.BuildARN("batch", "quota-share", shared.StrParam(params, "quotaShareName")), + "quotaShareName": shared.StrParam(params, "quotaShareName"), }) case "DeleteQuotaShare": return shared.JSONResponse(http.StatusOK, map[string]any{}) @@ -175,8 +175,8 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"quotaShares": []any{}, "nextToken": ""}) case "CreateServiceEnvironment": return shared.JSONResponse(http.StatusOK, map[string]any{ - "serviceEnvironmentArn": shared.BuildARN("batch", "service-environment", strParam(params, "serviceEnvironmentName")), - "serviceEnvironmentName": strParam(params, "serviceEnvironmentName"), + "serviceEnvironmentArn": shared.BuildARN("batch", "service-environment", shared.StrParam(params, "serviceEnvironmentName")), + "serviceEnvironmentName": shared.StrParam(params, "serviceEnvironmentName"), }) case "DeleteServiceEnvironment": return shared.JSONResponse(http.StatusOK, map[string]any{}) @@ -189,7 +189,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{ "jobArn": shared.BuildARN("batch", "job", jobID), "jobId": jobID, - "jobName": strParam(params, "jobName"), + "jobName": shared.StrParam(params, "jobName"), }) case "TerminateServiceJob": return shared.JSONResponse(http.StatusOK, map[string]any{}) @@ -291,20 +291,16 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- ComputeEnvironment CRUD --- func (p *Provider) createComputeEnvironment(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "computeEnvironmentName") + name := shared.StrParam(params, "computeEnvironmentName") if name == "" { return shared.JSONError("ClientException", "computeEnvironmentName is required", http.StatusBadRequest), nil } ceType := strParamDefault(params, "type", "MANAGED") state := strParamDefault(params, "state", "ENABLED") - serviceRole := strParam(params, "serviceRole") + serviceRole := shared.StrParam(params, "serviceRole") computeResources := "{}" if cr, ok := params["computeResources"]; ok { @@ -368,7 +364,7 @@ func (p *Provider) describeComputeEnvironments(params map[string]any) (*plugin.R } func (p *Provider) updateComputeEnvironment(params map[string]any) (*plugin.Response, error) { - nameOrARN := strParam(params, "computeEnvironment") + nameOrARN := shared.StrParam(params, "computeEnvironment") if nameOrARN == "" { return shared.JSONError("ClientException", "computeEnvironment is required", http.StatusBadRequest), nil } @@ -389,7 +385,7 @@ func (p *Provider) updateComputeEnvironment(params map[string]any) (*plugin.Resp } func (p *Provider) deleteComputeEnvironment(params map[string]any) (*plugin.Response, error) { - nameOrARN := strParam(params, "computeEnvironment") + nameOrARN := shared.StrParam(params, "computeEnvironment") if nameOrARN == "" { return shared.JSONError("ClientException", "computeEnvironment is required", http.StatusBadRequest), nil } @@ -407,7 +403,7 @@ func (p *Provider) deleteComputeEnvironment(params map[string]any) (*plugin.Resp // --- JobQueue CRUD --- func (p *Provider) createJobQueue(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "jobQueueName") + name := shared.StrParam(params, "jobQueueName") if name == "" { return shared.JSONError("ClientException", "jobQueueName is required", http.StatusBadRequest), nil } @@ -416,7 +412,7 @@ func (p *Provider) createJobQueue(params map[string]any) (*plugin.Response, erro if v, ok := params["priority"].(float64); ok { priority = int32(v) } - schedulingPolicy := strParam(params, "schedulingPolicyArn") + schedulingPolicy := shared.StrParam(params, "schedulingPolicyArn") computeEnvs := "[]" if ce, ok := params["computeEnvironmentOrder"]; ok { @@ -480,7 +476,7 @@ func (p *Provider) describeJobQueues(params map[string]any) (*plugin.Response, e } func (p *Provider) updateJobQueue(params map[string]any) (*plugin.Response, error) { - nameOrARN := strParam(params, "jobQueue") + nameOrARN := shared.StrParam(params, "jobQueue") if nameOrARN == "" { return shared.JSONError("ClientException", "jobQueue is required", http.StatusBadRequest), nil } @@ -505,7 +501,7 @@ func (p *Provider) updateJobQueue(params map[string]any) (*plugin.Response, erro } func (p *Provider) deleteJobQueue(params map[string]any) (*plugin.Response, error) { - nameOrARN := strParam(params, "jobQueue") + nameOrARN := shared.StrParam(params, "jobQueue") if nameOrARN == "" { return shared.JSONError("ClientException", "jobQueue is required", http.StatusBadRequest), nil } @@ -523,7 +519,7 @@ func (p *Provider) deleteJobQueue(params map[string]any) (*plugin.Response, erro // --- JobDefinition CRUD --- func (p *Provider) registerJobDefinition(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "jobDefinitionName") + name := shared.StrParam(params, "jobDefinitionName") if name == "" { return shared.JSONError("ClientException", "jobDefinitionName is required", http.StatusBadRequest), nil } @@ -575,8 +571,8 @@ func (p *Provider) registerJobDefinition(params map[string]any) (*plugin.Respons } func (p *Provider) describeJobDefinitions(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "jobDefinitionName") - status := strParam(params, "status") + name := shared.StrParam(params, "jobDefinitionName") + status := shared.StrParam(params, "status") arnFilters := toStringSlice(params["jobDefinitions"]) var jds []*JobDefinition @@ -609,7 +605,7 @@ func (p *Provider) describeJobDefinitions(params map[string]any) (*plugin.Respon } func (p *Provider) deregisterJobDefinition(params map[string]any) (*plugin.Response, error) { - nameOrARN := strParam(params, "jobDefinition") + nameOrARN := shared.StrParam(params, "jobDefinition") if nameOrARN == "" { return shared.JSONError("ClientException", "jobDefinition is required", http.StatusBadRequest), nil } @@ -622,12 +618,12 @@ func (p *Provider) deregisterJobDefinition(params map[string]any) (*plugin.Respo // --- Jobs --- func (p *Provider) submitJob(params map[string]any) (*plugin.Response, error) { - jobName := strParam(params, "jobName") + jobName := shared.StrParam(params, "jobName") if jobName == "" { return shared.JSONError("ClientException", "jobName is required", http.StatusBadRequest), nil } - jobQueue := strParam(params, "jobQueue") - jobDefinition := strParam(params, "jobDefinition") + jobQueue := shared.StrParam(params, "jobQueue") + jobDefinition := shared.StrParam(params, "jobDefinition") parameters := "{}" if p2, ok := params["parameters"]; ok { @@ -687,8 +683,8 @@ func (p *Provider) describeJobs(params map[string]any) (*plugin.Response, error) } func (p *Provider) listJobs(params map[string]any) (*plugin.Response, error) { - queue := strParam(params, "jobQueue") - status := strParam(params, "jobStatus") + queue := shared.StrParam(params, "jobQueue") + status := shared.StrParam(params, "jobStatus") jobs, err := p.store.ListJobs(queue, status) if err != nil { @@ -705,7 +701,7 @@ func (p *Provider) listJobs(params map[string]any) (*plugin.Response, error) { } func (p *Provider) cancelJob(params map[string]any) (*plugin.Response, error) { - jobID := strParam(params, "jobId") + jobID := shared.StrParam(params, "jobId") reason := strParamDefault(params, "reason", "cancelled") if jobID == "" { return shared.JSONError("ClientException", "jobId is required", http.StatusBadRequest), nil @@ -717,7 +713,7 @@ func (p *Provider) cancelJob(params map[string]any) (*plugin.Response, error) { } func (p *Provider) terminateJob(params map[string]any) (*plugin.Response, error) { - jobID := strParam(params, "jobId") + jobID := shared.StrParam(params, "jobId") reason := strParamDefault(params, "reason", "terminated") if jobID == "" { return shared.JSONError("ClientException", "jobId is required", http.StatusBadRequest), nil @@ -731,7 +727,7 @@ func (p *Provider) terminateJob(params map[string]any) (*plugin.Response, error) // --- SchedulingPolicy CRUD --- func (p *Provider) createSchedulingPolicy(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") + name := shared.StrParam(params, "name") if name == "" { return shared.JSONError("ClientException", "name is required", http.StatusBadRequest), nil } @@ -803,7 +799,7 @@ func (p *Provider) listSchedulingPolicies() (*plugin.Response, error) { } func (p *Provider) updateSchedulingPolicy(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "arn") + arn := shared.StrParam(params, "arn") if arn == "" { return shared.JSONError("ClientException", "arn is required", http.StatusBadRequest), nil } @@ -818,7 +814,7 @@ func (p *Provider) updateSchedulingPolicy(params map[string]any) (*plugin.Respon } func (p *Provider) deleteSchedulingPolicy(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "arn") + arn := shared.StrParam(params, "arn") if arn == "" { return shared.JSONError("ClientException", "arn is required", http.StatusBadRequest), nil } @@ -838,7 +834,7 @@ func (p *Provider) deleteSchedulingPolicy(params map[string]any) (*plugin.Respon func (p *Provider) tagResource(req *http.Request, params map[string]any) (*plugin.Response, error) { arn := extractPathParam(req.URL.Path, "tags") if arn == "" { - arn = strParam(params, "resourceArn") + arn = shared.StrParam(params, "resourceArn") } if arn == "" { return shared.JSONError("ClientException", "resourceArn is required", http.StatusBadRequest), nil @@ -995,13 +991,6 @@ func spToDetailMap(sp *SchedulingPolicy, tags map[string]string) map[string]any // --- Util --- -func strParam(params map[string]any, key string) string { - if v, ok := params[key].(string); ok { - return v - } - return "" -} - func strParamDefault(params map[string]any, key, def string) string { if v, ok := params[key].(string); ok && v != "" { return v diff --git a/internal/services/bedrock/provider.go b/internal/services/bedrock/provider.go index 0e0e1054..0ac50568 100644 --- a/internal/services/bedrock/provider.go +++ b/internal/services/bedrock/provider.go @@ -20,7 +20,7 @@ import ( const ( defaultAccountID = plugin.DefaultAccountID - defaultRegion = "us-east-1" + defaultRegion = shared.DefaultRegion ) // hardcoded foundation models @@ -223,10 +223,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Foundation Models --- func (p *Provider) listFoundationModels(_ url.Values) (*plugin.Response, error) { @@ -256,9 +252,9 @@ func (p *Provider) invokeModel(modelID string, _ bool) (*plugin.Response, error) func (p *Provider) createModelCustomizationJob(body map[string]any) (*plugin.Response, error) { jobID := shared.GenerateUUID() - jobName := strVal(body, "jobName") - customModelName := strVal(body, "customModelName") - baseModelID := strVal(body, "baseModelIdentifier") + jobName := shared.StrParam(body, "jobName") + customModelName := shared.StrParam(body, "customModelName") + baseModelID := shared.StrParam(body, "baseModelIdentifier") j := &CustomizationJob{ JobID: jobID, @@ -377,8 +373,8 @@ func (p *Provider) deleteCustomModel(modelID string) (*plugin.Response, error) { // --- Guardrails --- func (p *Provider) createGuardrail(body map[string]any) (*plugin.Response, error) { - name := strVal(body, "name") - description := strVal(body, "description") + name := shared.StrParam(body, "name") + description := shared.StrParam(body, "description") guardrailID := shared.GenerateID("", 10) g := &Guardrail{ @@ -447,11 +443,11 @@ func (p *Provider) updateGuardrail(guardrailID string, body map[string]any) (*pl if err != nil { return shared.JSONError("ResourceNotFoundException", "guardrail not found", http.StatusNotFound), nil } - name := strVal(body, "name") + name := shared.StrParam(body, "name") if name == "" { name = g.Name } - description := strVal(body, "description") + description := shared.StrParam(body, "description") if err := p.store.UpdateGuardrail(guardrailID, name, description); err != nil { return shared.JSONError("InternalError", err.Error(), http.StatusInternalServerError), nil } @@ -520,15 +516,6 @@ func (p *Provider) untagResource(arn string, keys []string) (*plugin.Response, e // --- helpers --- -func strVal(m map[string]any, key string) string { - if v, ok := m[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func extractSegment(path, prefix string) string { s := strings.TrimPrefix(path, prefix) // Remove trailing slash segments beyond first diff --git a/internal/services/cloudcontrol/provider.go b/internal/services/cloudcontrol/provider.go index e7bc69ef..8c194adb 100644 --- a/internal/services/cloudcontrol/provider.go +++ b/internal/services/cloudcontrol/provider.go @@ -153,10 +153,6 @@ func (p *CloudControlProvider) ListResources(_ context.Context) ([]plugin.Resour return []plugin.Resource{}, nil } -func (p *CloudControlProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Operations --- func (p *CloudControlProvider) createResource(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/cloudformation/provider.go b/internal/services/cloudformation/provider.go index 3470d1ef..cb6f3302 100644 --- a/internal/services/cloudformation/provider.go +++ b/internal/services/cloudformation/provider.go @@ -223,10 +223,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ============================================================ // Stack handlers // ============================================================ diff --git a/internal/services/cloudfront/provider.go b/internal/services/cloudfront/provider.go index 9ac793cc..738a6c60 100644 --- a/internal/services/cloudfront/provider.go +++ b/internal/services/cloudfront/provider.go @@ -178,10 +178,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func cfError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/cloudsearch/provider.go b/internal/services/cloudsearch/provider.go index efc7d51a..ff46bf9b 100644 --- a/internal/services/cloudsearch/provider.go +++ b/internal/services/cloudsearch/provider.go @@ -141,10 +141,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func csError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/cloudtrail/provider.go b/internal/services/cloudtrail/provider.go index e5fbcda9..0d69c798 100644 --- a/internal/services/cloudtrail/provider.go +++ b/internal/services/cloudtrail/provider.go @@ -186,10 +186,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Trail handlers ---- func (p *Provider) createTrail(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/cloudwatch/provider.go b/internal/services/cloudwatch/provider.go index e6e6782a..72b65ecc 100644 --- a/internal/services/cloudwatch/provider.go +++ b/internal/services/cloudwatch/provider.go @@ -119,10 +119,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return resources, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // putMetricData parses MetricData.member.N.* form fields. func (p *Provider) putMetricData(jm bool, req *http.Request) (*plugin.Response, error) { namespace := req.FormValue("Namespace") diff --git a/internal/services/cloudwatchlogs/provider.go b/internal/services/cloudwatchlogs/provider.go index 4e49d021..993a2d2b 100644 --- a/internal/services/cloudwatchlogs/provider.go +++ b/internal/services/cloudwatchlogs/provider.go @@ -137,10 +137,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return resources, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) createLogGroup(params map[string]any) (*plugin.Response, error) { name, _ := params["logGroupName"].(string) if name == "" { diff --git a/internal/services/codeartifact/provider.go b/internal/services/codeartifact/provider.go index d6886f5e..3d2176c5 100644 --- a/internal/services/codeartifact/provider.go +++ b/internal/services/codeartifact/provider.go @@ -345,10 +345,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Domain --- func (p *Provider) createDomain(req *http.Request, params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/codebuild/provider.go b/internal/services/codebuild/provider.go index f4496748..b2eb4ddd 100644 --- a/internal/services/codebuild/provider.go +++ b/internal/services/codebuild/provider.go @@ -153,10 +153,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Project handlers ---- func (p *Provider) createProject(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/codecommit/provider.go b/internal/services/codecommit/provider.go index b3b57a57..0e139906 100644 --- a/internal/services/codecommit/provider.go +++ b/internal/services/codecommit/provider.go @@ -212,10 +212,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Repository handlers ---- func (p *Provider) createRepository(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/codeconnections/provider.go b/internal/services/codeconnections/provider.go index 11402d73..8c4863a3 100644 --- a/internal/services/codeconnections/provider.go +++ b/internal/services/codeconnections/provider.go @@ -135,7 +135,7 @@ func (p *CodeConnectionsProvider) HandleRequest(_ context.Context, op string, re case "GetSyncBlockerSummary": return shared.JSONResponse(http.StatusOK, map[string]any{ "SyncBlockerSummary": map[string]any{ - "ResourceName": getString(params, "ResourceName"), + "ResourceName": shared.StrParam(params, "ResourceName"), "ParentResourceName": "", "LatestBlockers": []any{}, }, @@ -149,22 +149,22 @@ func (p *CodeConnectionsProvider) HandleRequest(_ context.Context, op string, re return shared.JSONResponse(http.StatusOK, map[string]any{ "SyncBlocker": map[string]any{ "Id": shared.GenerateID("blocker-", 16), - "Type": getString(params, "Type"), + "Type": shared.StrParam(params, "Type"), "Status": "ACTIVE", }, }) case "UpdateSyncBlocker": return shared.JSONResponse(http.StatusOK, map[string]any{ - "ResourceName": getString(params, "ResourceName"), + "ResourceName": shared.StrParam(params, "ResourceName"), "ParentResourceName": "", "SyncBlocker": map[string]any{ - "Id": getString(params, "Id"), + "Id": shared.StrParam(params, "Id"), "Status": "RESOLVED", }, }) case "StartRepositoryLink": return shared.JSONResponse(http.StatusOK, map[string]any{ - "RepositoryLinkId": getString(params, "RepositoryLinkId"), + "RepositoryLinkId": shared.StrParam(params, "RepositoryLinkId"), }) case "GetRepositorySyncStatus": return shared.JSONResponse(http.StatusOK, map[string]any{ @@ -221,11 +221,6 @@ func (p *CodeConnectionsProvider) ListResources(_ context.Context) ([]plugin.Res return res, nil } -// GetMetrics returns empty metrics. -func (p *CodeConnectionsProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Connection handlers --- func (p *CodeConnectionsProvider) createConnection(params map[string]any) (*plugin.Response, error) { @@ -717,11 +712,6 @@ func parseTagList(raw []any) map[string]string { return tags } -func getString(params map[string]any, key string) string { - s, _ := params[key].(string) - return s -} - func init() { plugin.DefaultRegistry.Register("codeconnections", func() plugin.ServicePlugin { return &CodeConnectionsProvider{} diff --git a/internal/services/codedeploy/provider.go b/internal/services/codedeploy/provider.go index c224ca79..98b27de5 100644 --- a/internal/services/codedeploy/provider.go +++ b/internal/services/codedeploy/provider.go @@ -142,10 +142,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Application handlers ---- func (p *Provider) createApplication(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/codepipeline/provider.go b/internal/services/codepipeline/provider.go index 8b8c1743..3ebabb72 100644 --- a/internal/services/codepipeline/provider.go +++ b/internal/services/codepipeline/provider.go @@ -205,10 +205,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Pipeline handlers ---- func (p *Provider) createPipeline(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/cognitoidentity/provider.go b/internal/services/cognitoidentity/provider.go index 66aa7446..1fc41e23 100644 --- a/internal/services/cognitoidentity/provider.go +++ b/internal/services/cognitoidentity/provider.go @@ -13,10 +13,12 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID -const defaultRegion = "us-east-1" +const defaultRegion = shared.DefaultRegion // Provider implements the CognitoIdentityService using JSON 1.1 protocol. type Provider struct { @@ -114,10 +116,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func arnForPool(accountID, region, id string) string { diff --git a/internal/services/cognitoidentityprovider/provider.go b/internal/services/cognitoidentityprovider/provider.go index 11cc8e90..6ffe7b80 100644 --- a/internal/services/cognitoidentityprovider/provider.go +++ b/internal/services/cognitoidentityprovider/provider.go @@ -19,7 +19,7 @@ import ( ) const defaultAccountID = plugin.DefaultAccountID -const defaultRegion = "us-east-1" +const defaultRegion = shared.DefaultRegion // Provider implements the CognitoIdentityProviderService service. type Provider struct { @@ -199,10 +199,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func jsonResp(status int, v any) (*plugin.Response, error) { diff --git a/internal/services/configservice/provider.go b/internal/services/configservice/provider.go index 55d4cdb0..8d5f0437 100644 --- a/internal/services/configservice/provider.go +++ b/internal/services/configservice/provider.go @@ -203,21 +203,8 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- helpers ---- -func strParam(params map[string]any, key string) string { - if v, ok := params[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func intParam(params map[string]any, key string, def int) int { if v, ok := params[key]; ok { switch n := v.(type) { @@ -296,8 +283,8 @@ func tagsFromParams(params map[string]any, key string) map[string]string { out := make(map[string]string, len(arr)) for _, item := range arr { if m, ok := item.(map[string]any); ok { - k := strParam(m, "Key") - val := strParam(m, "Value") + k := shared.StrParam(m, "Key") + val := shared.StrParam(m, "Value") if k != "" { out[k] = val } @@ -341,7 +328,7 @@ func (p *Provider) putConfigRule(params map[string]any) (*plugin.Response, error if input == nil { return shared.JSONError("InvalidParameterValueException", "ConfigRule is required", http.StatusBadRequest), nil } - name := strParam(input, "ConfigRuleName") + name := shared.StrParam(input, "ConfigRuleName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigRuleName is required", http.StatusBadRequest), nil } @@ -350,7 +337,7 @@ func (p *Provider) putConfigRule(params map[string]any) (*plugin.Response, error ARN: buildConfigRuleARN(name), Source: marshalParam(input, "Source"), Scope: marshalParam(input, "Scope"), - InputParams: strParam(input, "InputParameters"), + InputParams: shared.StrParam(input, "InputParameters"), State: "ACTIVE", CreatedAt: now(), } @@ -374,7 +361,7 @@ func (p *Provider) describeConfigRules(params map[string]any) (*plugin.Response, } func (p *Provider) deleteConfigRule(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConfigRuleName") + name := shared.StrParam(params, "ConfigRuleName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigRuleName is required", http.StatusBadRequest), nil } @@ -426,22 +413,22 @@ func (p *Provider) putConfigurationRecorder(params map[string]any) (*plugin.Resp if input == nil { return shared.JSONError("InvalidParameterValueException", "ConfigurationRecorder is required", http.StatusBadRequest), nil } - name := strParam(input, "name") + name := shared.StrParam(input, "name") if name == "" { - name = strParam(input, "Name") + name = shared.StrParam(input, "Name") } if name == "" { name = "default" } r := &ConfigurationRecorder{ Name: name, - RoleARN: strParam(input, "roleARN"), + RoleARN: shared.StrParam(input, "roleARN"), RecordingGroup: marshalParam(input, "recordingGroup"), RecordingMode: marshalParam(input, "recordingMode"), Status: "STOPPED", } if r.RoleARN == "" { - r.RoleARN = strParam(input, "RoleARN") + r.RoleARN = shared.StrParam(input, "RoleARN") } if err := p.store.PutConfigurationRecorder(r); err != nil { return nil, err @@ -479,7 +466,7 @@ func (p *Provider) describeConfigurationRecorderStatus(params map[string]any) (* } func (p *Provider) startConfigurationRecorder(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConfigurationRecorderName") + name := shared.StrParam(params, "ConfigurationRecorderName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigurationRecorderName is required", http.StatusBadRequest), nil } @@ -493,7 +480,7 @@ func (p *Provider) startConfigurationRecorder(params map[string]any) (*plugin.Re } func (p *Provider) stopConfigurationRecorder(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConfigurationRecorderName") + name := shared.StrParam(params, "ConfigurationRecorderName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigurationRecorderName is required", http.StatusBadRequest), nil } @@ -507,7 +494,7 @@ func (p *Provider) stopConfigurationRecorder(params map[string]any) (*plugin.Res } func (p *Provider) deleteConfigurationRecorder(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConfigurationRecorderName") + name := shared.StrParam(params, "ConfigurationRecorderName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigurationRecorderName is required", http.StatusBadRequest), nil } @@ -540,22 +527,22 @@ func (p *Provider) putDeliveryChannel(params map[string]any) (*plugin.Response, if input == nil { return shared.JSONError("InvalidParameterValueException", "DeliveryChannel is required", http.StatusBadRequest), nil } - name := strParam(input, "name") + name := shared.StrParam(input, "name") if name == "" { - name = strParam(input, "Name") + name = shared.StrParam(input, "Name") } if name == "" { name = "default" } d := &DeliveryChannel{ Name: name, - S3Bucket: strParam(input, "s3BucketName"), - S3Prefix: strParam(input, "s3KeyPrefix"), - SNSTopic: strParam(input, "snsTopicARN"), + S3Bucket: shared.StrParam(input, "s3BucketName"), + S3Prefix: shared.StrParam(input, "s3KeyPrefix"), + SNSTopic: shared.StrParam(input, "snsTopicARN"), Frequency: "TwentyFour_Hours", } if freq, ok := input["configSnapshotDeliveryProperties"].(map[string]any); ok { - if f := strParam(freq, "deliveryFrequency"); f != "" { + if f := shared.StrParam(freq, "deliveryFrequency"); f != "" { d.Frequency = f } } @@ -600,7 +587,7 @@ func (p *Provider) describeDeliveryChannelStatus(params map[string]any) (*plugin } func (p *Provider) deleteDeliveryChannel(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "DeliveryChannelName") + name := shared.StrParam(params, "DeliveryChannelName") if name == "" { return shared.JSONError("InvalidParameterValueException", "DeliveryChannelName is required", http.StatusBadRequest), nil } @@ -629,15 +616,15 @@ func deliveryChannelToMap(d *DeliveryChannel) map[string]any { // ---- ConformancePack handlers ---- func (p *Provider) putConformancePack(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConformancePackName") + name := shared.StrParam(params, "ConformancePackName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConformancePackName is required", http.StatusBadRequest), nil } c := &ConformancePack{ Name: name, ARN: buildConformancePackARN(name), - TemplateBody: strParam(params, "TemplateBody"), - DeliveryBucket: strParam(params, "DeliveryS3Bucket"), + TemplateBody: shared.StrParam(params, "TemplateBody"), + DeliveryBucket: shared.StrParam(params, "DeliveryS3Bucket"), Status: "CREATE_COMPLETE", CreatedAt: now(), } @@ -679,7 +666,7 @@ func (p *Provider) describeConformancePackStatus(params map[string]any) (*plugin } func (p *Provider) describeConformancePackCompliance(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConformancePackName") + name := shared.StrParam(params, "ConformancePackName") _ = name return shared.JSONResponse(http.StatusOK, map[string]any{ "ConformancePackName": name, @@ -688,7 +675,7 @@ func (p *Provider) describeConformancePackCompliance(params map[string]any) (*pl } func (p *Provider) deleteConformancePack(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConformancePackName") + name := shared.StrParam(params, "ConformancePackName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConformancePackName is required", http.StatusBadRequest), nil } @@ -715,7 +702,7 @@ func conformancePackToMap(c *ConformancePack) map[string]any { // ---- ConfigurationAggregator handlers ---- func (p *Provider) putConfigurationAggregator(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConfigurationAggregatorName") + name := shared.StrParam(params, "ConfigurationAggregatorName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigurationAggregatorName is required", http.StatusBadRequest), nil } @@ -748,7 +735,7 @@ func (p *Provider) describeConfigurationAggregators(params map[string]any) (*plu } func (p *Provider) describeConfigurationAggregatorSourcesStatus(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConfigurationAggregatorName") + name := shared.StrParam(params, "ConfigurationAggregatorName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigurationAggregatorName is required", http.StatusBadRequest), nil } @@ -763,7 +750,7 @@ func (p *Provider) describeConfigurationAggregatorSourcesStatus(params map[strin } func (p *Provider) deleteConfigurationAggregator(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConfigurationAggregatorName") + name := shared.StrParam(params, "ConfigurationAggregatorName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigurationAggregatorName is required", http.StatusBadRequest), nil } @@ -796,7 +783,7 @@ func (p *Provider) putStoredQuery(params map[string]any) (*plugin.Response, erro if input == nil { return shared.JSONError("InvalidParameterValueException", "StoredQuery is required", http.StatusBadRequest), nil } - name := strParam(input, "QueryName") + name := shared.StrParam(input, "QueryName") if name == "" { return shared.JSONError("InvalidParameterValueException", "QueryName is required", http.StatusBadRequest), nil } @@ -804,8 +791,8 @@ func (p *Provider) putStoredQuery(params map[string]any) (*plugin.Response, erro Name: name, ID: shared.GenerateUUID(), ARN: buildStoredQueryARN(name), - Expression: strParam(input, "Expression"), - Description: strParam(input, "Description"), + Expression: shared.StrParam(input, "Expression"), + Description: shared.StrParam(input, "Description"), } // Keep existing ID if present existing, _ := p.store.GetStoredQuery(name) @@ -820,7 +807,7 @@ func (p *Provider) putStoredQuery(params map[string]any) (*plugin.Response, erro } func (p *Provider) getStoredQuery(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "QueryName") + name := shared.StrParam(params, "QueryName") if name == "" { return shared.JSONError("InvalidParameterValueException", "QueryName is required", http.StatusBadRequest), nil } @@ -851,7 +838,7 @@ func (p *Provider) listStoredQueries(_ map[string]any) (*plugin.Response, error) } func (p *Provider) deleteStoredQuery(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "QueryName") + name := shared.StrParam(params, "QueryName") if name == "" { return shared.JSONError("InvalidParameterValueException", "QueryName is required", http.StatusBadRequest), nil } @@ -904,7 +891,7 @@ func (p *Provider) describeRetentionConfigurations(_ map[string]any) (*plugin.Re } func (p *Provider) deleteRetentionConfiguration(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "RetentionConfigurationName") + name := shared.StrParam(params, "RetentionConfigurationName") if name == "" { name = "default" } @@ -928,8 +915,8 @@ func retentionConfigToMap(r *RetentionConfig) map[string]any { // ---- AggregationAuthorization handlers ---- func (p *Provider) putAggregationAuthorization(params map[string]any) (*plugin.Response, error) { - account := strParam(params, "AuthorizedAccountId") - region := strParam(params, "AuthorizedAwsRegion") + account := shared.StrParam(params, "AuthorizedAccountId") + region := shared.StrParam(params, "AuthorizedAwsRegion") if account == "" || region == "" { return shared.JSONError("InvalidParameterValueException", "AuthorizedAccountId and AuthorizedAwsRegion are required", http.StatusBadRequest), nil } @@ -960,8 +947,8 @@ func (p *Provider) describeAggregationAuthorizations(_ map[string]any) (*plugin. } func (p *Provider) deleteAggregationAuthorization(params map[string]any) (*plugin.Response, error) { - account := strParam(params, "AuthorizedAccountId") - region := strParam(params, "AuthorizedAwsRegion") + account := shared.StrParam(params, "AuthorizedAccountId") + region := shared.StrParam(params, "AuthorizedAwsRegion") if account == "" || region == "" { return shared.JSONError("InvalidParameterValueException", "AuthorizedAccountId and AuthorizedAwsRegion are required", http.StatusBadRequest), nil } @@ -992,14 +979,14 @@ func (p *Provider) putRemediationConfigurations(params map[string]any) (*plugin. if !ok { continue } - ruleName := strParam(m, "ConfigRuleName") + ruleName := shared.StrParam(m, "ConfigRuleName") if ruleName == "" { continue } r := &RemediationConfig{ ConfigRuleName: ruleName, - TargetType: strParam(m, "TargetType"), - TargetID: strParam(m, "TargetId"), + TargetType: shared.StrParam(m, "TargetType"), + TargetID: shared.StrParam(m, "TargetId"), Parameters: marshalParam(m, "Parameters"), } if r.TargetType == "" { @@ -1029,7 +1016,7 @@ func (p *Provider) describeRemediationConfigurations(params map[string]any) (*pl } func (p *Provider) deleteRemediationConfiguration(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConfigRuleName") + name := shared.StrParam(params, "ConfigRuleName") if name == "" { return shared.JSONError("InvalidParameterValueException", "ConfigRuleName is required", http.StatusBadRequest), nil } @@ -1058,7 +1045,7 @@ func remediationConfigToMap(r *RemediationConfig) map[string]any { // ---- Tags handlers ---- func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") if arn == "" { return shared.JSONError("InvalidParameterValueException", "ResourceArn is required", http.StatusBadRequest), nil } @@ -1070,7 +1057,7 @@ func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) } func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") if arn == "" { return shared.JSONError("InvalidParameterValueException", "ResourceArn is required", http.StatusBadRequest), nil } @@ -1082,7 +1069,7 @@ func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error } func (p *Provider) listTagsForResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") if arn == "" { return shared.JSONError("InvalidParameterValueException", "ResourceArn is required", http.StatusBadRequest), nil } diff --git a/internal/services/configservice/store.go b/internal/services/configservice/store.go index a41dfd44..516460ca 100644 --- a/internal/services/configservice/store.go +++ b/internal/services/configservice/store.go @@ -205,7 +205,7 @@ func (s *Store) PutConfigRule(r *ConfigRule) error { return err } -func scanConfigRule(row shared.Scanner) (*ConfigRule, error) { +func scanConfigRule(row sqlite.Scanner) (*ConfigRule, error) { r := &ConfigRule{} err := row.Scan(&r.Name, &r.ARN, &r.Source, &r.Scope, &r.InputParams, &r.State, &r.CreatedAt) if err == sql.ErrNoRows { @@ -274,7 +274,7 @@ func (s *Store) PutConfigurationRecorder(r *ConfigurationRecorder) error { return err } -func scanRecorder(row shared.Scanner) (*ConfigurationRecorder, error) { +func scanRecorder(row sqlite.Scanner) (*ConfigurationRecorder, error) { r := &ConfigurationRecorder{} err := row.Scan(&r.Name, &r.RoleARN, &r.RecordingGroup, &r.RecordingMode, &r.Status) if err == sql.ErrNoRows { @@ -355,7 +355,7 @@ func (s *Store) PutDeliveryChannel(d *DeliveryChannel) error { return err } -func scanDeliveryChannel(row shared.Scanner) (*DeliveryChannel, error) { +func scanDeliveryChannel(row sqlite.Scanner) (*DeliveryChannel, error) { d := &DeliveryChannel{} err := row.Scan(&d.Name, &d.S3Bucket, &d.S3Prefix, &d.SNSTopic, &d.Frequency) if err == sql.ErrNoRows { @@ -418,7 +418,7 @@ func (s *Store) PutConformancePack(c *ConformancePack) error { return err } -func scanConformancePack(row shared.Scanner) (*ConformancePack, error) { +func scanConformancePack(row sqlite.Scanner) (*ConformancePack, error) { c := &ConformancePack{} err := row.Scan(&c.Name, &c.ARN, &c.TemplateBody, &c.DeliveryBucket, &c.Status, &c.CreatedAt) if err == sql.ErrNoRows { @@ -480,7 +480,7 @@ func (s *Store) PutAggregator(a *Aggregator) error { return err } -func scanAggregator(row shared.Scanner) (*Aggregator, error) { +func scanAggregator(row sqlite.Scanner) (*Aggregator, error) { a := &Aggregator{} err := row.Scan(&a.Name, &a.ARN, &a.AccountSources, &a.OrgSource, &a.CreatedAt) if err == sql.ErrNoRows { @@ -548,7 +548,7 @@ func (s *Store) PutStoredQuery(q *StoredQuery) error { return err } -func scanStoredQuery(row shared.Scanner) (*StoredQuery, error) { +func scanStoredQuery(row sqlite.Scanner) (*StoredQuery, error) { q := &StoredQuery{} err := row.Scan(&q.Name, &q.ID, &q.ARN, &q.Expression, &q.Description) if err == sql.ErrNoRows { @@ -600,7 +600,7 @@ func (s *Store) PutRetentionConfig(r *RetentionConfig) error { return err } -func scanRetentionConfig(row shared.Scanner) (*RetentionConfig, error) { +func scanRetentionConfig(row sqlite.Scanner) (*RetentionConfig, error) { r := &RetentionConfig{} err := row.Scan(&r.Name, &r.RetentionDays) if err == sql.ErrNoRows { @@ -654,7 +654,7 @@ func (s *Store) PutAggregationAuthorization(a *AggregationAuthorization) error { return err } -func scanAggAuth(row shared.Scanner) (*AggregationAuthorization, error) { +func scanAggAuth(row sqlite.Scanner) (*AggregationAuthorization, error) { a := &AggregationAuthorization{} err := row.Scan(&a.ARN, &a.AuthorizedAccount, &a.AuthorizedRegion, &a.CreatedAt) if err == sql.ErrNoRows { @@ -705,7 +705,7 @@ func (s *Store) PutRemediationConfig(r *RemediationConfig) error { return err } -func scanRemediationConfig(row shared.Scanner) (*RemediationConfig, error) { +func scanRemediationConfig(row sqlite.Scanner) (*RemediationConfig, error) { r := &RemediationConfig{} err := row.Scan(&r.ConfigRuleName, &r.TargetType, &r.TargetID, &r.Parameters, &r.Automatic) if err == sql.ErrNoRows { diff --git a/internal/services/costexplorer/provider.go b/internal/services/costexplorer/provider.go index 6addebe2..0ae3edc1 100644 --- a/internal/services/costexplorer/provider.go +++ b/internal/services/costexplorer/provider.go @@ -189,10 +189,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ----- CostCategoryDefinition ----- func (p *Provider) createCostCategoryDefinition(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/dms/provider.go b/internal/services/dms/provider.go index f8d73ed0..33c4f7f3 100644 --- a/internal/services/dms/provider.go +++ b/internal/services/dms/provider.go @@ -105,7 +105,7 @@ func (p *DMSProvider) HandleRequest(_ context.Context, op string, req *http.Requ return p.reloadTables(params) case "DescribeTableStatistics": return shared.JSONResponse(http.StatusOK, map[string]any{ - "ReplicationTaskArn": getString(params, "ReplicationTaskArn"), + "ReplicationTaskArn": shared.StrParam(params, "ReplicationTaskArn"), "TableStatistics": []any{}, }) case "DescribeReplicationTaskAssessmentResults": @@ -200,11 +200,6 @@ func (p *DMSProvider) ListResources(_ context.Context) ([]plugin.Resource, error return out, nil } -// GetMetrics returns empty metrics. -func (p *DMSProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Replication Instance handlers --- func (p *DMSProvider) createReplicationInstance(params map[string]any) (*plugin.Response, error) { @@ -780,11 +775,6 @@ func parseDMSTags(raw []any) map[string]string { return tags } -func getString(params map[string]any, key string) string { - s, _ := params[key].(string) - return s -} - func init() { plugin.DefaultRegistry.Register("dms", func() plugin.ServicePlugin { return &DMSProvider{} diff --git a/internal/services/docdb/provider.go b/internal/services/docdb/provider.go index 9e5957cd..461054c9 100644 --- a/internal/services/docdb/provider.go +++ b/internal/services/docdb/provider.go @@ -206,10 +206,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func dbError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/dynamodb/provider.go b/internal/services/dynamodb/provider.go index d6d18cc9..0debf611 100644 --- a/internal/services/dynamodb/provider.go +++ b/internal/services/dynamodb/provider.go @@ -119,14 +119,6 @@ func (p *DynamoDBProvider) ListResources(_ context.Context) ([]plugin.Resource, return resources, nil } -// GetMetrics returns basic metrics for the DynamoDB provider. -func (p *DynamoDBProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - names := p.store.ListTables() - return &plugin.ServiceMetrics{ - ResourceCount: len(names), - }, nil -} - // --- request/response structs --- type keySchemaElement struct { diff --git a/internal/services/dynamodbstreams/provider.go b/internal/services/dynamodbstreams/provider.go index 4d626c8d..a962ecee 100644 --- a/internal/services/dynamodbstreams/provider.go +++ b/internal/services/dynamodbstreams/provider.go @@ -136,14 +136,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - streams, err := p.store.ListStreams("") - if err != nil { - return &plugin.ServiceMetrics{}, nil - } - return &plugin.ServiceMetrics{ResourceCount: len(streams)}, nil -} - // ---- handlers ---- func (p *Provider) listStreams(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/ebs/provider.go b/internal/services/ebs/provider.go index 6de39f7a..a5dd5df4 100644 --- a/internal/services/ebs/provider.go +++ b/internal/services/ebs/provider.go @@ -95,10 +95,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Operations --- func (p *Provider) startSnapshot(req *http.Request) (*plugin.Response, error) { diff --git a/internal/services/ec2/provider.go b/internal/services/ec2/provider.go index ca17a9c9..16be4038 100644 --- a/internal/services/ec2/provider.go +++ b/internal/services/ec2/provider.go @@ -15,6 +15,8 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const ( @@ -215,10 +217,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- operation handlers --- func (p *Provider) handleRunInstances(form url.Values) (*plugin.Response, error) { @@ -889,7 +887,7 @@ func (p *Provider) handleDescribeAvailabilityZones(_ url.Values) (*plugin.Respon XMLName xml.Name `xml:"DescribeAvailabilityZonesResponse"` AvailabilityZoneInfo []azXML `xml:"availabilityZoneInfo>item"` } - const region = "us-east-1" + const region = shared.DefaultRegion suffixes := []string{"a", "b", "c", "d", "f"} items := make([]azXML, 0, len(suffixes)) for i, sfx := range suffixes { diff --git a/internal/services/ecr/provider.go b/internal/services/ecr/provider.go index 3b00cde1..ad7a57a0 100644 --- a/internal/services/ecr/provider.go +++ b/internal/services/ecr/provider.go @@ -15,6 +15,8 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID @@ -145,14 +147,10 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- operation handlers --- func (p *Provider) handleCreateRepository(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "repositoryName") + name := shared.StrParam(params, "repositoryName") if name == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -167,7 +165,7 @@ func (p *Provider) handleCreateRepository(params map[string]any) (*plugin.Respon } func (p *Provider) handleDeleteRepository(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "repositoryName") + name := shared.StrParam(params, "repositoryName") if name == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -202,9 +200,9 @@ func (p *Provider) handleDescribeRepositories(params map[string]any) (*plugin.Re } func (p *Provider) handlePutImage(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") - manifest := strParam(params, "imageManifest") - tag := strParam(params, "imageTag") + repoName := shared.StrParam(params, "repositoryName") + manifest := shared.StrParam(params, "imageManifest") + tag := shared.StrParam(params, "imageTag") if repoName == "" || manifest == "" { return ecrError("InvalidParameterException", "repositoryName and imageManifest are required", http.StatusBadRequest), nil } @@ -216,7 +214,7 @@ func (p *Provider) handlePutImage(params map[string]any) (*plugin.Response, erro } func (p *Provider) handleBatchGetImage(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") imageIDs := imageIDsParam(params, "imageIds") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil @@ -233,7 +231,7 @@ func (p *Provider) handleBatchGetImage(params map[string]any) (*plugin.Response, } func (p *Provider) handleBatchDeleteImage(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") imageIDs := imageIDsParam(params, "imageIds") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil @@ -250,7 +248,7 @@ func (p *Provider) handleBatchDeleteImage(params map[string]any) (*plugin.Respon } func (p *Provider) handleDescribeImages(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -266,7 +264,7 @@ func (p *Provider) handleDescribeImages(params map[string]any) (*plugin.Response } func (p *Provider) handleListImages(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -300,8 +298,8 @@ func (p *Provider) handleGetAuthorizationToken(_ map[string]any) (*plugin.Respon } func (p *Provider) handleSetRepositoryPolicy(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") - policyText := strParam(params, "policyText") + repoName := shared.StrParam(params, "repositoryName") + policyText := shared.StrParam(params, "policyText") if repoName == "" || policyText == "" { return ecrError("InvalidParameterException", "repositoryName and policyText are required", http.StatusBadRequest), nil } @@ -319,7 +317,7 @@ func (p *Provider) handleSetRepositoryPolicy(params map[string]any) (*plugin.Res } func (p *Provider) handleGetRepositoryPolicy(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -343,7 +341,7 @@ func (p *Provider) handleGetRepositoryPolicy(params map[string]any) (*plugin.Res // --- layer operation handlers --- func (p *Provider) handleInitiateLayerUpload(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -358,8 +356,8 @@ func (p *Provider) handleInitiateLayerUpload(params map[string]any) (*plugin.Res } func (p *Provider) handleUploadLayerPart(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") - uploadID := strParam(params, "uploadId") + repoName := shared.StrParam(params, "repositoryName") + uploadID := shared.StrParam(params, "uploadId") if repoName == "" || uploadID == "" { return ecrError("InvalidParameterException", "repositoryName and uploadId are required", http.StatusBadRequest), nil } @@ -397,8 +395,8 @@ func (p *Provider) handleUploadLayerPart(params map[string]any) (*plugin.Respons } func (p *Provider) handleCompleteLayerUpload(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") - uploadID := strParam(params, "uploadId") + repoName := shared.StrParam(params, "repositoryName") + uploadID := shared.StrParam(params, "uploadId") if repoName == "" || uploadID == "" { return ecrError("InvalidParameterException", "repositoryName and uploadId are required", http.StatusBadRequest), nil } @@ -422,7 +420,7 @@ func (p *Provider) handleCompleteLayerUpload(params map[string]any) (*plugin.Res } func (p *Provider) handleBatchCheckLayerAvailability(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -439,8 +437,8 @@ func (p *Provider) handleBatchCheckLayerAvailability(params map[string]any) (*pl } func (p *Provider) handleGetDownloadUrlForLayer(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") - digest := strParam(params, "layerDigest") + repoName := shared.StrParam(params, "repositoryName") + digest := shared.StrParam(params, "layerDigest") if repoName == "" || digest == "" { return ecrError("InvalidParameterException", "repositoryName and layerDigest are required", http.StatusBadRequest), nil } @@ -462,8 +460,8 @@ func (p *Provider) handleGetDownloadUrlForLayer(params map[string]any) (*plugin. // --- lifecycle policy handlers --- func (p *Provider) handlePutLifecyclePolicy(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") - policyText := strParam(params, "lifecyclePolicyText") + repoName := shared.StrParam(params, "repositoryName") + policyText := shared.StrParam(params, "lifecyclePolicyText") if repoName == "" || policyText == "" { return ecrError("InvalidParameterException", "repositoryName and lifecyclePolicyText are required", http.StatusBadRequest), nil } @@ -481,7 +479,7 @@ func (p *Provider) handlePutLifecyclePolicy(params map[string]any) (*plugin.Resp } func (p *Provider) handleGetLifecyclePolicy(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -503,7 +501,7 @@ func (p *Provider) handleGetLifecyclePolicy(params map[string]any) (*plugin.Resp } func (p *Provider) handleDeleteLifecyclePolicy(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -530,7 +528,7 @@ func (p *Provider) handleDeleteLifecyclePolicy(params map[string]any) (*plugin.R // --- tag handlers --- func (p *Provider) handleTagResource(params map[string]any) (*plugin.Response, error) { - resourceARN := strParam(params, "resourceArn") + resourceARN := shared.StrParam(params, "resourceArn") if resourceARN == "" { return ecrError("InvalidParameterException", "resourceArn is required", http.StatusBadRequest), nil } @@ -542,7 +540,7 @@ func (p *Provider) handleTagResource(params map[string]any) (*plugin.Response, e } func (p *Provider) handleUntagResource(params map[string]any) (*plugin.Response, error) { - resourceARN := strParam(params, "resourceArn") + resourceARN := shared.StrParam(params, "resourceArn") if resourceARN == "" { return ecrError("InvalidParameterException", "resourceArn is required", http.StatusBadRequest), nil } @@ -554,7 +552,7 @@ func (p *Provider) handleUntagResource(params map[string]any) (*plugin.Response, } func (p *Provider) handleListTagsForResource(params map[string]any) (*plugin.Response, error) { - resourceARN := strParam(params, "resourceArn") + resourceARN := shared.StrParam(params, "resourceArn") if resourceARN == "" { return ecrError("InvalidParameterException", "resourceArn is required", http.StatusBadRequest), nil } @@ -572,7 +570,7 @@ func (p *Provider) handleListTagsForResource(params map[string]any) (*plugin.Res // --- image scan handlers --- func (p *Provider) handleStartImageScan(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -607,7 +605,7 @@ func (p *Provider) handleStartImageScan(params map[string]any) (*plugin.Response } func (p *Provider) handleDescribeImageScanFindings(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -653,7 +651,7 @@ func (p *Provider) handleDescribeImageScanFindings(params map[string]any) (*plug } func (p *Provider) handlePutImageScanningConfiguration(params map[string]any) (*plugin.Response, error) { - repoName := strParam(params, "repositoryName") + repoName := shared.StrParam(params, "repositoryName") if repoName == "" { return ecrError("InvalidParameterException", "repositoryName is required", http.StatusBadRequest), nil } @@ -693,15 +691,6 @@ func ecrJSON(status int, v any) (*plugin.Response, error) { return &plugin.Response{StatusCode: status, ContentType: "application/x-amz-json-1.1", Body: body}, nil } -func strParam(params map[string]any, key string) string { - if v, ok := params[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func strSliceParam(params map[string]any, key string) []string { v, ok := params[key] if !ok { diff --git a/internal/services/ecr/store.go b/internal/services/ecr/store.go index 45007a4e..0dec9548 100644 --- a/internal/services/ecr/store.go +++ b/internal/services/ecr/store.go @@ -27,7 +27,7 @@ var ( ErrLayerUploadNotFound = errors.New("layer upload not found") ) -const region = "us-east-1" +const region = shared.DefaultRegion var migrations = []sqlite.Migration{ {Version: 1, SQL: ` diff --git a/internal/services/ecs/provider.go b/internal/services/ecs/provider.go index b797997f..b4631f2c 100644 --- a/internal/services/ecs/provider.go +++ b/internal/services/ecs/provider.go @@ -13,6 +13,8 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID @@ -209,14 +211,10 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- operation handlers --- func (p *Provider) handleCreateCluster(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "clusterName") + name := shared.StrParam(params, "clusterName") if name == "" { name = "default" } @@ -275,7 +273,7 @@ func (p *Provider) handleDescribeClusters(params map[string]any) (*plugin.Respon } func (p *Provider) handleDeleteCluster(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "cluster") + arn := shared.StrParam(params, "cluster") if arn == "" { return ecsError("MissingParameter", "cluster is required", http.StatusBadRequest), nil } @@ -286,7 +284,7 @@ func (p *Provider) handleDeleteCluster(params map[string]any) (*plugin.Response, } func (p *Provider) handleRegisterTaskDefinition(params map[string]any) (*plugin.Response, error) { - family := strParam(params, "family") + family := shared.StrParam(params, "family") if family == "" { return ecsError("MissingParameter", "family is required", http.StatusBadRequest), nil } @@ -304,7 +302,7 @@ func (p *Provider) handleRegisterTaskDefinition(params map[string]any) (*plugin. } func (p *Provider) handleDescribeTaskDefinition(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "taskDefinition") + arn := shared.StrParam(params, "taskDefinition") if arn == "" { return ecsError("MissingParameter", "taskDefinition is required", http.StatusBadRequest), nil } @@ -327,7 +325,7 @@ func (p *Provider) handleListTaskDefinitions(_ map[string]any) (*plugin.Response } func (p *Provider) handleDeregisterTaskDefinition(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "taskDefinition") + arn := shared.StrParam(params, "taskDefinition") if arn == "" { return ecsError("MissingParameter", "taskDefinition is required", http.StatusBadRequest), nil } @@ -339,8 +337,8 @@ func (p *Provider) handleDeregisterTaskDefinition(params map[string]any) (*plugi } func (p *Provider) handleRunTask(params map[string]any) (*plugin.Response, error) { - clusterArn := strParam(params, "cluster") - taskDefArn := strParam(params, "taskDefinition") + clusterArn := shared.StrParam(params, "cluster") + taskDefArn := shared.StrParam(params, "taskDefinition") if clusterArn == "" || taskDefArn == "" { return ecsError("MissingParameter", "cluster and taskDefinition are required", http.StatusBadRequest), nil } @@ -352,7 +350,7 @@ func (p *Provider) handleRunTask(params map[string]any) (*plugin.Response, error } func (p *Provider) handleStopTask(params map[string]any) (*plugin.Response, error) { - taskArn := strParam(params, "task") + taskArn := shared.StrParam(params, "task") if taskArn == "" { return ecsError("MissingParameter", "task is required", http.StatusBadRequest), nil } @@ -376,9 +374,9 @@ func (p *Provider) handleDescribeTasks(params map[string]any) (*plugin.Response, } func (p *Provider) handleCreateService(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "serviceName") - clusterArn := strParam(params, "cluster") - taskDefArn := strParam(params, "taskDefinition") + name := shared.StrParam(params, "serviceName") + clusterArn := shared.StrParam(params, "cluster") + taskDefArn := shared.StrParam(params, "taskDefinition") if name == "" || clusterArn == "" || taskDefArn == "" { return ecsError("MissingParameter", "serviceName, cluster, and taskDefinition are required", http.StatusBadRequest), nil } @@ -394,9 +392,9 @@ func (p *Provider) handleCreateService(params map[string]any) (*plugin.Response, } func (p *Provider) handleUpdateService(params map[string]any) (*plugin.Response, error) { - serviceRef := strParam(params, "service") - clusterArn := strParam(params, "cluster") - taskDefArn := strParam(params, "taskDefinition") + serviceRef := shared.StrParam(params, "service") + clusterArn := shared.StrParam(params, "cluster") + taskDefArn := shared.StrParam(params, "taskDefinition") desiredCount := intParam(params, "desiredCount", -1) if serviceRef == "" { return ecsError("MissingParameter", "service is required", http.StatusBadRequest), nil @@ -418,7 +416,7 @@ func (p *Provider) handleUpdateService(params map[string]any) (*plugin.Response, } func (p *Provider) handleDeleteService(params map[string]any) (*plugin.Response, error) { - serviceArn := strParam(params, "service") + serviceArn := shared.StrParam(params, "service") if serviceArn == "" { return ecsError("MissingParameter", "service is required", http.StatusBadRequest), nil } @@ -429,7 +427,7 @@ func (p *Provider) handleDeleteService(params map[string]any) (*plugin.Response, } func (p *Provider) handleListServices(params map[string]any) (*plugin.Response, error) { - clusterArn := strParam(params, "cluster") + clusterArn := shared.StrParam(params, "cluster") if clusterArn == "" { return ecsError("MissingParameter", "cluster is required", http.StatusBadRequest), nil } @@ -446,7 +444,7 @@ func (p *Provider) handleListServices(params map[string]any) (*plugin.Response, // --- Capacity Provider handlers --- func (p *Provider) handleCreateCapacityProvider(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") + name := shared.StrParam(params, "name") if name == "" { return ecsError("MissingParameter", "name is required", http.StatusBadRequest), nil } @@ -475,7 +473,7 @@ func (p *Provider) handleDescribeCapacityProviders(params map[string]any) (*plug } func (p *Provider) handleDeleteCapacityProvider(params map[string]any) (*plugin.Response, error) { - nameOrARN := strParam(params, "capacityProvider") + nameOrARN := shared.StrParam(params, "capacityProvider") if nameOrARN == "" { return ecsError("MissingParameter", "capacityProvider is required", http.StatusBadRequest), nil } @@ -486,7 +484,7 @@ func (p *Provider) handleDeleteCapacityProvider(params map[string]any) (*plugin. } func (p *Provider) handlePutClusterCapacityProviders(params map[string]any) (*plugin.Response, error) { - clusterRef := strParam(params, "cluster") + clusterRef := shared.StrParam(params, "cluster") if clusterRef == "" { return ecsError("MissingParameter", "cluster is required", http.StatusBadRequest), nil } @@ -505,7 +503,7 @@ func (p *Provider) handlePutClusterCapacityProviders(params map[string]any) (*pl // --- Container Instance handlers --- func (p *Provider) handleListContainerInstances(params map[string]any) (*plugin.Response, error) { - clusterRef := strParam(params, "cluster") + clusterRef := shared.StrParam(params, "cluster") if clusterRef == "" { return ecsError("MissingParameter", "cluster is required", http.StatusBadRequest), nil } @@ -513,7 +511,7 @@ func (p *Provider) handleListContainerInstances(params map[string]any) (*plugin. if !strings.Contains(clusterRef, ":") { clusterARN = fmt.Sprintf("arn:aws:ecs:%s:%s:cluster/%s", region, defaultAccountID, clusterRef) } - status := strParam(params, "status") + status := shared.StrParam(params, "status") arns, err := p.store.ListContainerInstances(defaultAccountID, clusterARN, status) if err != nil { return nil, err @@ -538,7 +536,7 @@ func (p *Provider) handleDescribeContainerInstances(params map[string]any) (*plu } func (p *Provider) handleDeregisterContainerInstance(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "containerInstance") + arn := shared.StrParam(params, "containerInstance") if arn == "" { return ecsError("MissingParameter", "containerInstance is required", http.StatusBadRequest), nil } @@ -555,7 +553,7 @@ func (p *Provider) handleDeregisterContainerInstance(params map[string]any) (*pl // --- Attribute handlers --- func (p *Provider) handlePutAttributes(params map[string]any) (*plugin.Response, error) { - clusterRef := strParam(params, "cluster") + clusterRef := shared.StrParam(params, "cluster") clusterARN := clusterRef if clusterRef != "" && !strings.Contains(clusterRef, ":") { clusterARN = fmt.Sprintf("arn:aws:ecs:%s:%s:cluster/%s", region, defaultAccountID, clusterRef) @@ -573,7 +571,7 @@ func (p *Provider) handlePutAttributes(params map[string]any) (*plugin.Response, } func (p *Provider) handleDeleteAttributes(params map[string]any) (*plugin.Response, error) { - clusterRef := strParam(params, "cluster") + clusterRef := shared.StrParam(params, "cluster") clusterARN := clusterRef if clusterRef != "" && !strings.Contains(clusterRef, ":") { clusterARN = fmt.Sprintf("arn:aws:ecs:%s:%s:cluster/%s", region, defaultAccountID, clusterRef) @@ -587,14 +585,14 @@ func (p *Provider) handleDeleteAttributes(params map[string]any) (*plugin.Respon } func (p *Provider) handleListAttributes(params map[string]any) (*plugin.Response, error) { - clusterRef := strParam(params, "cluster") + clusterRef := shared.StrParam(params, "cluster") clusterARN := clusterRef if clusterRef != "" && !strings.Contains(clusterRef, ":") { clusterARN = fmt.Sprintf("arn:aws:ecs:%s:%s:cluster/%s", region, defaultAccountID, clusterRef) } - targetType := strParam(params, "targetType") - attrName := strParam(params, "attributeName") - attrValue := strParam(params, "attributeValue") + targetType := shared.StrParam(params, "targetType") + attrName := shared.StrParam(params, "attributeName") + attrValue := shared.StrParam(params, "attributeValue") attrs, err := p.store.ListAttributes(defaultAccountID, clusterARN, targetType, attrName, attrValue) if err != nil { return nil, err @@ -610,7 +608,7 @@ func (p *Provider) handleListAttributes(params map[string]any) (*plugin.Response func (p *Provider) handleUpdateContainerInstancesState(params map[string]any) (*plugin.Response, error) { arns := strSliceParam(params, "containerInstances") - status := strParam(params, "status") + status := shared.StrParam(params, "status") if status == "" { return ecsError("MissingParameter", "status is required", http.StatusBadRequest), nil } @@ -671,15 +669,6 @@ func ecsJSON(status int, v any) (*plugin.Response, error) { return &plugin.Response{StatusCode: status, ContentType: "application/x-amz-json-1.1", Body: body}, nil } -func strParam(params map[string]any, key string) string { - if v, ok := params[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func intParam(params map[string]any, key string, defaultVal int) int { if v, ok := params[key]; ok { switch n := v.(type) { @@ -820,7 +809,7 @@ func parseAttributes(raw []any) []ECSAttribute { // --- Cluster extras --- func (p *Provider) handleUpdateCluster(params map[string]any) (*plugin.Response, error) { - clusterRef := strParam(params, "cluster") + clusterRef := shared.StrParam(params, "cluster") if clusterRef == "" { return ecsError("MissingParameter", "cluster is required", http.StatusBadRequest), nil } @@ -869,8 +858,8 @@ func (p *Provider) handleListTaskDefinitionFamilies(_ map[string]any) (*plugin.R // --- Task extras --- func (p *Provider) handleStartTask(params map[string]any) (*plugin.Response, error) { - clusterArn := strParam(params, "cluster") - taskDefArn := strParam(params, "taskDefinition") + clusterArn := shared.StrParam(params, "cluster") + taskDefArn := shared.StrParam(params, "taskDefinition") if clusterArn == "" || taskDefArn == "" { return ecsError("MissingParameter", "cluster and taskDefinition are required", http.StatusBadRequest), nil } @@ -882,7 +871,7 @@ func (p *Provider) handleStartTask(params map[string]any) (*plugin.Response, err } func (p *Provider) handleListTasks(params map[string]any) (*plugin.Response, error) { - clusterArn := strParam(params, "cluster") + clusterArn := shared.StrParam(params, "cluster") // ListTasks for a given cluster just returns all RUNNING tasks in that cluster. tasks, err := p.store.DescribeTasks(defaultAccountID, nil) if err != nil { @@ -901,7 +890,7 @@ func (p *Provider) handleListTasks(params map[string]any) (*plugin.Response, err func (p *Provider) handleDescribeServices(params map[string]any) (*plugin.Response, error) { arns := strSliceParam(params, "services") - clusterRef := strParam(params, "cluster") + clusterRef := shared.StrParam(params, "cluster") // Resolve service names to ARNs within cluster. resolved := make([]string, 0, len(arns)) for _, a := range arns { @@ -931,7 +920,7 @@ func (p *Provider) handleDescribeServices(params map[string]any) (*plugin.Respon // --- Capacity Provider extras --- func (p *Provider) handleUpdateCapacityProvider(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") + name := shared.StrParam(params, "name") if name == "" { return ecsError("MissingParameter", "name is required", http.StatusBadRequest), nil } @@ -945,7 +934,7 @@ func (p *Provider) handleUpdateCapacityProvider(params map[string]any) (*plugin. // --- Container Instance extras --- func (p *Provider) handleRegisterContainerInstance(params map[string]any) (*plugin.Response, error) { - clusterRef := strParam(params, "cluster") + clusterRef := shared.StrParam(params, "cluster") if clusterRef == "" { clusterRef = "default" } @@ -957,7 +946,7 @@ func (p *Provider) handleRegisterContainerInstance(params map[string]any) (*plug } else { clusterARN = fmt.Sprintf("arn:aws:ecs:%s:%s:cluster/%s", region, defaultAccountID, clusterRef) } - ec2ID := strParam(params, "instanceIdentityDocument") + ec2ID := shared.StrParam(params, "instanceIdentityDocument") ci, err := p.store.RegisterContainerInstance(defaultAccountID, clusterARN, clusterName, ec2ID) if err != nil { return nil, err @@ -968,11 +957,11 @@ func (p *Provider) handleRegisterContainerInstance(params map[string]any) (*plug // --- Task Sets --- func (p *Provider) handleCreateTaskSet(params map[string]any) (*plugin.Response, error) { - serviceARN := strParam(params, "service") - clusterARN := strParam(params, "cluster") - taskDefARN := strParam(params, "taskDefinition") - externalID := strParam(params, "externalId") - launchType := strParam(params, "launchType") + serviceARN := shared.StrParam(params, "service") + clusterARN := shared.StrParam(params, "cluster") + taskDefARN := shared.StrParam(params, "taskDefinition") + externalID := shared.StrParam(params, "externalId") + launchType := shared.StrParam(params, "launchType") if serviceARN == "" || clusterARN == "" || taskDefARN == "" { return ecsError("MissingParameter", "service, cluster, and taskDefinition are required", http.StatusBadRequest), nil } @@ -984,7 +973,7 @@ func (p *Provider) handleCreateTaskSet(params map[string]any) (*plugin.Response, } func (p *Provider) handleDeleteTaskSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "taskSet") + id := shared.StrParam(params, "taskSet") if id == "" { return ecsError("MissingParameter", "taskSet is required", http.StatusBadRequest), nil } @@ -995,7 +984,7 @@ func (p *Provider) handleDeleteTaskSet(params map[string]any) (*plugin.Response, } func (p *Provider) handleUpdateTaskSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "taskSet") + id := shared.StrParam(params, "taskSet") if id == "" { return ecsError("MissingParameter", "taskSet is required", http.StatusBadRequest), nil } @@ -1017,7 +1006,7 @@ func (p *Provider) handleUpdateTaskSet(params map[string]any) (*plugin.Response, } func (p *Provider) handleDescribeTaskSets(params map[string]any) (*plugin.Response, error) { - serviceARN := strParam(params, "service") + serviceARN := shared.StrParam(params, "service") sets, err := p.store.ListTaskSets(defaultAccountID, serviceARN) if err != nil { return nil, err @@ -1030,7 +1019,7 @@ func (p *Provider) handleDescribeTaskSets(params map[string]any) (*plugin.Respon } func (p *Provider) handleUpdateServicePrimaryTaskSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "primaryTaskSet") + id := shared.StrParam(params, "primaryTaskSet") if id == "" { return ecsError("MissingParameter", "primaryTaskSet is required", http.StatusBadRequest), nil } @@ -1044,17 +1033,17 @@ func (p *Provider) handleUpdateServicePrimaryTaskSet(params map[string]any) (*pl // --- Service Auto-Scaling --- func (p *Provider) handleRegisterScalableTarget(params map[string]any) (*plugin.Response, error) { - serviceARN := strParam(params, "serviceARN") + serviceARN := shared.StrParam(params, "serviceARN") if serviceARN == "" { - serviceARN = strParam(params, "resourceId") + serviceARN = shared.StrParam(params, "resourceId") } - dimension := strParam(params, "scalableDimension") + dimension := shared.StrParam(params, "scalableDimension") if serviceARN == "" || dimension == "" { return ecsError("InvalidParameterException", "serviceARN and scalableDimension are required", http.StatusBadRequest), nil } minCap := intParam(params, "minCapacity", 0) maxCap := intParam(params, "maxCapacity", 10) - roleARN := strParam(params, "roleARN") + roleARN := shared.StrParam(params, "roleARN") if _, err := p.store.RegisterScalableTarget(defaultAccountID, serviceARN, dimension, roleARN, minCap, maxCap); err != nil { return nil, err } @@ -1062,11 +1051,11 @@ func (p *Provider) handleRegisterScalableTarget(params map[string]any) (*plugin. } func (p *Provider) handleDeregisterScalableTarget(params map[string]any) (*plugin.Response, error) { - serviceARN := strParam(params, "serviceARN") + serviceARN := shared.StrParam(params, "serviceARN") if serviceARN == "" { - serviceARN = strParam(params, "resourceId") + serviceARN = shared.StrParam(params, "resourceId") } - dimension := strParam(params, "scalableDimension") + dimension := shared.StrParam(params, "scalableDimension") if err := p.store.DeregisterScalableTarget(defaultAccountID, serviceARN, dimension); err != nil { return ecsError("ObjectNotFoundException", "scalable target not found", http.StatusBadRequest), nil } @@ -1074,11 +1063,11 @@ func (p *Provider) handleDeregisterScalableTarget(params map[string]any) (*plugi } func (p *Provider) handleDescribeScalableTargets(params map[string]any) (*plugin.Response, error) { - serviceARN := strParam(params, "serviceARN") + serviceARN := shared.StrParam(params, "serviceARN") if serviceARN == "" { - serviceARN = strParam(params, "resourceId") + serviceARN = shared.StrParam(params, "resourceId") } - dimension := strParam(params, "scalableDimension") + dimension := shared.StrParam(params, "scalableDimension") targets, err := p.store.ListScalableTargets(defaultAccountID, serviceARN, dimension) if err != nil { return nil, err @@ -1091,16 +1080,16 @@ func (p *Provider) handleDescribeScalableTargets(params map[string]any) (*plugin } func (p *Provider) handlePutScalingPolicy(params map[string]any) (*plugin.Response, error) { - serviceARN := strParam(params, "serviceARN") + serviceARN := shared.StrParam(params, "serviceARN") if serviceARN == "" { - serviceARN = strParam(params, "resourceId") + serviceARN = shared.StrParam(params, "resourceId") } - policyName := strParam(params, "policyName") - dimension := strParam(params, "scalableDimension") + policyName := shared.StrParam(params, "policyName") + dimension := shared.StrParam(params, "scalableDimension") if serviceARN == "" || policyName == "" || dimension == "" { return ecsError("InvalidParameterException", "serviceARN, policyName, and scalableDimension are required", http.StatusBadRequest), nil } - policyType := strParam(params, "policyType") + policyType := shared.StrParam(params, "policyType") if policyType == "" { policyType = "TargetTrackingScaling" } @@ -1120,11 +1109,11 @@ func (p *Provider) handlePutScalingPolicy(params map[string]any) (*plugin.Respon } func (p *Provider) handleDeleteScalingPolicy(params map[string]any) (*plugin.Response, error) { - serviceARN := strParam(params, "serviceARN") + serviceARN := shared.StrParam(params, "serviceARN") if serviceARN == "" { - serviceARN = strParam(params, "resourceId") + serviceARN = shared.StrParam(params, "resourceId") } - policyName := strParam(params, "policyName") + policyName := shared.StrParam(params, "policyName") if err := p.store.DeleteServiceScalingPolicy(defaultAccountID, serviceARN, policyName); err != nil { return ecsError("ObjectNotFoundException", "scaling policy not found", http.StatusBadRequest), nil } @@ -1132,9 +1121,9 @@ func (p *Provider) handleDeleteScalingPolicy(params map[string]any) (*plugin.Res } func (p *Provider) handleDescribeScalingPolicies(params map[string]any) (*plugin.Response, error) { - serviceARN := strParam(params, "serviceARN") + serviceARN := shared.StrParam(params, "serviceARN") if serviceARN == "" { - serviceARN = strParam(params, "resourceId") + serviceARN = shared.StrParam(params, "resourceId") } policies, err := p.store.ListServiceScalingPolicies(defaultAccountID, serviceARN) if err != nil { @@ -1150,7 +1139,7 @@ func (p *Provider) handleDescribeScalingPolicies(params map[string]any) (*plugin // --- Tags --- func (p *Provider) handleTagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "resourceArn") + arn := shared.StrParam(params, "resourceArn") if arn == "" { return ecsError("MissingParameter", "resourceArn is required", http.StatusBadRequest), nil } @@ -1174,7 +1163,7 @@ func (p *Provider) handleTagResource(params map[string]any) (*plugin.Response, e } func (p *Provider) handleUntagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "resourceArn") + arn := shared.StrParam(params, "resourceArn") if arn == "" { return ecsError("MissingParameter", "resourceArn is required", http.StatusBadRequest), nil } @@ -1186,7 +1175,7 @@ func (p *Provider) handleUntagResource(params map[string]any) (*plugin.Response, } func (p *Provider) handleListTagsForResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "resourceArn") + arn := shared.StrParam(params, "resourceArn") if arn == "" { return ecsError("MissingParameter", "resourceArn is required", http.StatusBadRequest), nil } @@ -1204,8 +1193,8 @@ func (p *Provider) handleListTagsForResource(params map[string]any) (*plugin.Res // --- Account Settings --- func (p *Provider) handleListAccountSettings(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") - principal := strParam(params, "principalArn") + name := shared.StrParam(params, "name") + principal := shared.StrParam(params, "principalArn") settings, err := p.store.ListAccountSettings(defaultAccountID, name, principal) if err != nil { return nil, err @@ -1222,9 +1211,9 @@ func (p *Provider) handleListAccountSettings(params map[string]any) (*plugin.Res } func (p *Provider) handlePutAccountSetting(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") - value := strParam(params, "value") - principal := strParam(params, "principalArn") + name := shared.StrParam(params, "name") + value := shared.StrParam(params, "value") + principal := shared.StrParam(params, "principalArn") if name == "" || value == "" { return ecsError("MissingParameter", "name and value are required", http.StatusBadRequest), nil } @@ -1244,8 +1233,8 @@ func (p *Provider) handlePutAccountSettingDefault(params map[string]any) (*plugi } func (p *Provider) handleDeleteAccountSetting(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") - principal := strParam(params, "principalArn") + name := shared.StrParam(params, "name") + principal := shared.StrParam(params, "principalArn") if err := p.store.DeleteAccountSetting(defaultAccountID, name, principal); err != nil { return nil, err } diff --git a/internal/services/ecs/store.go b/internal/services/ecs/store.go index ea3264ab..77b6c4de 100644 --- a/internal/services/ecs/store.go +++ b/internal/services/ecs/store.go @@ -29,7 +29,7 @@ var ( ErrTaskSetNotFound = errors.New("task set not found") ) -const region = "us-east-1" +const region = shared.DefaultRegion var migrations = []sqlite.Migration{ {Version: 1, SQL: ` diff --git a/internal/services/efs/provider.go b/internal/services/efs/provider.go index d2f26cf7..798a5950 100644 --- a/internal/services/efs/provider.go +++ b/internal/services/efs/provider.go @@ -19,7 +19,7 @@ import ( const ( defaultAccountID = plugin.DefaultAccountID - defaultRegion = "us-east-1" + defaultRegion = shared.DefaultRegion ) // Provider implements the MagnolioAPIService_v20150201 service. @@ -233,10 +233,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func efsARN(fsID string) string { @@ -317,7 +313,7 @@ func apToDesc(ap *accessPointRow, tags map[string]string) map[string]any { // --- FileSystem operations --- func (p *Provider) createFileSystem(body map[string]any) (*plugin.Response, error) { - token := strVal(body, "CreationToken") + token := shared.StrParam(body, "CreationToken") if token == "" { token = shared.GenerateUUID() } @@ -328,16 +324,16 @@ func (p *Provider) createFileSystem(body map[string]any) (*plugin.Response, erro } fsID := shared.GenerateID("fs-", 11) - perfMode := strVal(body, "PerformanceMode") + perfMode := shared.StrParam(body, "PerformanceMode") if perfMode == "" { perfMode = "generalPurpose" } - tpMode := strVal(body, "ThroughputMode") + tpMode := shared.StrParam(body, "ThroughputMode") if tpMode == "" { tpMode = "bursting" } encrypted := boolVal(body, "Encrypted") - kmsKey := strVal(body, "KmsKeyId") + kmsKey := shared.StrParam(body, "KmsKeyId") r := &fileSystemRow{ FileSystemID: fsID, @@ -393,7 +389,7 @@ func (p *Provider) updateFileSystem(fsID string, body map[string]any) (*plugin.R if err != nil { return shared.JSONError("FileSystemNotFound", "file system not found", http.StatusNotFound), nil } - tpMode := strVal(body, "ThroughputMode") + tpMode := shared.StrParam(body, "ThroughputMode") if tpMode == "" { tpMode = fs.ThroughputMode } @@ -416,7 +412,7 @@ func (p *Provider) putFileSystemPolicy(fsID string, body map[string]any) (*plugi if _, err := p.store.GetFileSystem(fsID); err != nil { return shared.JSONError("FileSystemNotFound", "file system not found", http.StatusNotFound), nil } - policy := strVal(body, "Policy") + policy := shared.StrParam(body, "Policy") if err := p.store.PutFileSystemPolicy(fsID, policy); err != nil { return shared.JSONError("InternalError", err.Error(), http.StatusInternalServerError), nil } @@ -444,7 +440,7 @@ func (p *Provider) putBackupPolicy(fsID string, body map[string]any) (*plugin.Re } status := "DISABLED" if bp, ok := body["BackupPolicy"].(map[string]any); ok { - status = strVal(bp, "Status") + status = shared.StrParam(bp, "Status") } if err := p.store.PutBackupPolicy(fsID, status); err != nil { return shared.JSONError("InternalError", err.Error(), http.StatusInternalServerError), nil @@ -496,12 +492,12 @@ func (p *Provider) describeLifecycleConfiguration(fsID string) (*plugin.Response // --- MountTarget operations --- func (p *Provider) createMountTarget(body map[string]any) (*plugin.Response, error) { - fsID := strVal(body, "FileSystemId") + fsID := shared.StrParam(body, "FileSystemId") if _, err := p.store.GetFileSystem(fsID); err != nil { return shared.JSONError("FileSystemNotFound", "file system not found", http.StatusNotFound), nil } - subnetID := strVal(body, "SubnetId") - ipAddr := strVal(body, "IpAddress") + subnetID := shared.StrParam(body, "SubnetId") + ipAddr := shared.StrParam(body, "IpAddress") if ipAddr == "" { ipAddr = "10.0.1.100" } @@ -591,12 +587,12 @@ func (p *Provider) modifyMountTargetSecurityGroups(mtID string, body map[string] // --- AccessPoint operations --- func (p *Provider) createAccessPoint(body map[string]any) (*plugin.Response, error) { - fsID := strVal(body, "FileSystemId") + fsID := shared.StrParam(body, "FileSystemId") if _, err := p.store.GetFileSystem(fsID); err != nil { return shared.JSONError("FileSystemNotFound", "file system not found", http.StatusNotFound), nil } apID := shared.GenerateID("fsap-", 13) - clientToken := strVal(body, "ClientToken") + clientToken := shared.StrParam(body, "ClientToken") var posixUser, rootDir string if pu, ok := body["PosixUser"]; ok { b, _ := json.Marshal(pu) @@ -700,15 +696,6 @@ func (p *Provider) untagResource(resourceID string, keys []string) (*plugin.Resp // --- helpers --- -func strVal(m map[string]any, key string) string { - if v, ok := m[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func boolVal(m map[string]any, key string) bool { if v, ok := m[key]; ok { if b, ok := v.(bool); ok { diff --git a/internal/services/eks/provider.go b/internal/services/eks/provider.go index 6c86669d..6688b85a 100644 --- a/internal/services/eks/provider.go +++ b/internal/services/eks/provider.go @@ -446,10 +446,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ── Cluster operations ────────────────────────────────────────────────────── func (p *Provider) createCluster(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/elasticache/provider.go b/internal/services/elasticache/provider.go index d259b646..32e1108c 100644 --- a/internal/services/elasticache/provider.go +++ b/internal/services/elasticache/provider.go @@ -194,10 +194,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func ecError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/elasticbeanstalk/provider.go b/internal/services/elasticbeanstalk/provider.go index 53160fa2..f052d066 100644 --- a/internal/services/elasticbeanstalk/provider.go +++ b/internal/services/elasticbeanstalk/provider.go @@ -168,10 +168,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func ebError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/elasticloadbalancingv2/provider.go b/internal/services/elasticloadbalancingv2/provider.go index 3f8c73e0..bbd5ca01 100644 --- a/internal/services/elasticloadbalancingv2/provider.go +++ b/internal/services/elasticloadbalancingv2/provider.go @@ -164,10 +164,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func elbError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/elasticsearchservice/provider.go b/internal/services/elasticsearchservice/provider.go index dc9532a8..6cb56f99 100644 --- a/internal/services/elasticsearchservice/provider.go +++ b/internal/services/elasticsearchservice/provider.go @@ -191,10 +191,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Core CRUD --- func (p *Provider) createDomain(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/emr/provider.go b/internal/services/emr/provider.go index 1c06dcd5..3d05c4a2 100644 --- a/internal/services/emr/provider.go +++ b/internal/services/emr/provider.go @@ -127,11 +127,11 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // Stub operations — return success/empty case "AddInstanceFleet": return shared.JSONResponse(http.StatusOK, map[string]any{ - "ClusterId": strParam(params, "ClusterId"), "ClusterArn": "", "InstanceFleetId": "", + "ClusterId": shared.StrParam(params, "ClusterId"), "ClusterArn": "", "InstanceFleetId": "", }) case "AddInstanceGroups": return shared.JSONResponse(http.StatusOK, map[string]any{ - "JobFlowId": strParam(params, "JobFlowId"), "ClusterArn": "", "InstanceGroupIds": []string{}, + "JobFlowId": shared.StrParam(params, "JobFlowId"), "ClusterArn": "", "InstanceGroupIds": []string{}, }) case "CreateStudioSessionMapping": return shared.JSONResponse(http.StatusOK, map[string]any{}) @@ -221,10 +221,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Cluster operations ---- func (p *Provider) runJobFlow(params map[string]any) (*plugin.Response, error) { @@ -853,11 +849,6 @@ func parseTags(rawTags []any) map[string]string { return tags } -func strParam(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func boolParam(params map[string]any, key string) bool { v, _ := params[key].(bool) return v diff --git a/internal/services/eventbridge/provider.go b/internal/services/eventbridge/provider.go index 4818e19b..82a871ea 100644 --- a/internal/services/eventbridge/provider.go +++ b/internal/services/eventbridge/provider.go @@ -142,10 +142,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return resources, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) createEventBus(params map[string]any) (*plugin.Response, error) { name, _ := params["Name"].(string) if name == "" { diff --git a/internal/services/eventbridge/store.go b/internal/services/eventbridge/store.go index 81888745..feeb9531 100644 --- a/internal/services/eventbridge/store.go +++ b/internal/services/eventbridge/store.go @@ -13,6 +13,8 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/storage/sqlite" + + "github.com/skyoo2003/devcloud/internal/plugin" ) var ( @@ -112,7 +114,7 @@ func NewEBStore(dataDir string) (*EBStore, error) { } eb := &EBStore{store: s} // Seed the default event bus. - _ = eb.CreateEventBus("default", "000000000000") + _ = eb.CreateEventBus("default", plugin.DefaultAccountID) return eb, nil } diff --git a/internal/services/firehose/provider.go b/internal/services/firehose/provider.go index 4f9d70f5..2a1fbf77 100644 --- a/internal/services/firehose/provider.go +++ b/internal/services/firehose/provider.go @@ -172,10 +172,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) createDeliveryStream(params map[string]any) (*plugin.Response, error) { name, _ := params["DeliveryStreamName"].(string) if name == "" { diff --git a/internal/services/fis/provider.go b/internal/services/fis/provider.go index 7b1f244f..c182ffac 100644 --- a/internal/services/fis/provider.go +++ b/internal/services/fis/provider.go @@ -242,10 +242,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- ExperimentTemplate CRUD --- func (p *Provider) createExperimentTemplate(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/glacier/provider.go b/internal/services/glacier/provider.go index 98a068cd..5313344f 100644 --- a/internal/services/glacier/provider.go +++ b/internal/services/glacier/provider.go @@ -333,10 +333,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Vault CRUD --- func (p *Provider) createVault(name string) (*plugin.Response, error) { diff --git a/internal/services/glue/provider.go b/internal/services/glue/provider.go index 8c3600b0..5b45d33c 100644 --- a/internal/services/glue/provider.go +++ b/internal/services/glue/provider.go @@ -230,21 +230,8 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- helpers ---- -func strParam(params map[string]any, key string) string { - if v, ok := params[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func intParam(params map[string]any, key string, def int) int { if v, ok := params[key]; ok { switch n := v.(type) { @@ -274,7 +261,7 @@ func marshalParamArray(params map[string]any, key string) string { } func catalogID(params map[string]any) string { - if id := strParam(params, "CatalogId"); id != "" { + if id := shared.StrParam(params, "CatalogId"); id != "" { return id } return shared.DefaultAccountID @@ -309,13 +296,13 @@ func (p *Provider) createDatabase(params map[string]any) (*plugin.Response, erro if input == nil { return shared.JSONError("InvalidInputException", "DatabaseInput is required", http.StatusBadRequest), nil } - name := strParam(input, "Name") + name := shared.StrParam(input, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } catID := catalogID(params) - description := strParam(input, "Description") - locationURI := strParam(input, "LocationUri") + description := shared.StrParam(input, "Description") + locationURI := shared.StrParam(input, "LocationUri") parameters := marshalParam(input, "Parameters") _, err := p.store.CreateDatabase(catID, name, description, locationURI, parameters) if err != nil { @@ -328,7 +315,7 @@ func (p *Provider) createDatabase(params map[string]any) (*plugin.Response, erro } func (p *Provider) getDatabase(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } @@ -357,7 +344,7 @@ func (p *Provider) getDatabases(params map[string]any) (*plugin.Response, error) } func (p *Provider) updateDatabase(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } @@ -366,8 +353,8 @@ func (p *Provider) updateDatabase(params map[string]any) (*plugin.Response, erro if input == nil { input = map[string]any{} } - description := strParam(input, "Description") - locationURI := strParam(input, "LocationUri") + description := shared.StrParam(input, "Description") + locationURI := shared.StrParam(input, "LocationUri") parameters := marshalParam(input, "Parameters") if err := p.store.UpdateDatabase(catID, name, description, locationURI, parameters); err != nil { if err == errDatabaseNotFound { @@ -379,7 +366,7 @@ func (p *Provider) updateDatabase(params map[string]any) (*plugin.Response, erro } func (p *Provider) deleteDatabase(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } @@ -409,7 +396,7 @@ func databaseToMap(db *Database) map[string]any { // ---- Table handlers ---- func (p *Provider) createTable(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") + dbName := shared.StrParam(params, "DatabaseName") if dbName == "" { return shared.JSONError("InvalidInputException", "DatabaseName is required", http.StatusBadRequest), nil } @@ -417,13 +404,13 @@ func (p *Provider) createTable(params map[string]any) (*plugin.Response, error) if input == nil { return shared.JSONError("InvalidInputException", "TableInput is required", http.StatusBadRequest), nil } - name := strParam(input, "Name") + name := shared.StrParam(input, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } catID := catalogID(params) - description := strParam(input, "Description") - tableType := strParam(input, "TableType") + description := shared.StrParam(input, "Description") + tableType := shared.StrParam(input, "TableType") if tableType == "" { tableType = "EXTERNAL_TABLE" } @@ -445,8 +432,8 @@ func (p *Provider) createTable(params map[string]any) (*plugin.Response, error) } func (p *Provider) getTable(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - name := strParam(params, "Name") + dbName := shared.StrParam(params, "DatabaseName") + name := shared.StrParam(params, "Name") if dbName == "" || name == "" { return shared.JSONError("InvalidInputException", "DatabaseName and Name are required", http.StatusBadRequest), nil } @@ -462,7 +449,7 @@ func (p *Provider) getTable(params map[string]any) (*plugin.Response, error) { } func (p *Provider) getTables(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") + dbName := shared.StrParam(params, "DatabaseName") if dbName == "" { return shared.JSONError("InvalidInputException", "DatabaseName is required", http.StatusBadRequest), nil } @@ -479,18 +466,18 @@ func (p *Provider) getTables(params map[string]any) (*plugin.Response, error) { } func (p *Provider) updateTable(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") + dbName := shared.StrParam(params, "DatabaseName") input, _ := params["TableInput"].(map[string]any) if dbName == "" || input == nil { return shared.JSONError("InvalidInputException", "DatabaseName and TableInput are required", http.StatusBadRequest), nil } - name := strParam(input, "Name") + name := shared.StrParam(input, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } catID := catalogID(params) - description := strParam(input, "Description") - tableType := strParam(input, "TableType") + description := shared.StrParam(input, "Description") + tableType := shared.StrParam(input, "TableType") if tableType == "" { tableType = "EXTERNAL_TABLE" } @@ -511,8 +498,8 @@ func (p *Provider) updateTable(params map[string]any) (*plugin.Response, error) } func (p *Provider) deleteTable(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - name := strParam(params, "Name") + dbName := shared.StrParam(params, "DatabaseName") + name := shared.StrParam(params, "Name") if dbName == "" || name == "" { return shared.JSONError("InvalidInputException", "DatabaseName and Name are required", http.StatusBadRequest), nil } @@ -527,7 +514,7 @@ func (p *Provider) deleteTable(params map[string]any) (*plugin.Response, error) } func (p *Provider) batchDeleteTable(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") + dbName := shared.StrParam(params, "DatabaseName") names := stringsParam(params, "TablesToDelete") catID := catalogID(params) var errs []any @@ -543,8 +530,8 @@ func (p *Provider) batchDeleteTable(params map[string]any) (*plugin.Response, er } func (p *Provider) getTableVersion(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") t, err := p.store.GetTable(catalogID(params), dbName, tableName) if err != nil { if err == errTableNotFound { @@ -561,8 +548,8 @@ func (p *Provider) getTableVersion(params map[string]any) (*plugin.Response, err } func (p *Provider) getTableVersions(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") t, err := p.store.GetTable(catalogID(params), dbName, tableName) if err != nil { if err == errTableNotFound { @@ -600,8 +587,8 @@ func tableToMap(t *Table) map[string]any { // ---- Partition handlers ---- func (p *Provider) createPartition(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") if dbName == "" || tableName == "" { return shared.JSONError("InvalidInputException", "DatabaseName and TableName are required", http.StatusBadRequest), nil } @@ -624,8 +611,8 @@ func (p *Provider) createPartition(params map[string]any) (*plugin.Response, err } func (p *Provider) getPartition(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") values := stringsParam(params, "PartitionValues") catID := catalogID(params) part, err := p.store.GetPartition(catID, dbName, tableName, values) @@ -639,8 +626,8 @@ func (p *Provider) getPartition(params map[string]any) (*plugin.Response, error) } func (p *Provider) getPartitions(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") catID := catalogID(params) parts, err := p.store.ListPartitions(catID, dbName, tableName) if err != nil { @@ -654,8 +641,8 @@ func (p *Provider) getPartitions(params map[string]any) (*plugin.Response, error } func (p *Provider) updatePartition(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") values := stringsParam(params, "PartitionValueList") input, _ := params["PartitionInput"].(map[string]any) if input == nil { @@ -674,8 +661,8 @@ func (p *Provider) updatePartition(params map[string]any) (*plugin.Response, err } func (p *Provider) batchCreatePartition(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") catID := catalogID(params) inputs, _ := params["PartitionInputList"].([]any) var errs []any @@ -701,8 +688,8 @@ func (p *Provider) batchCreatePartition(params map[string]any) (*plugin.Response } func (p *Provider) batchDeletePartition(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") catID := catalogID(params) parts, _ := params["PartitionsToDelete"].([]any) var errs []any @@ -726,8 +713,8 @@ func (p *Provider) batchDeletePartition(params map[string]any) (*plugin.Response } func (p *Provider) batchGetPartition(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") catID := catalogID(params) partitionsToGet, _ := params["PartitionsToGet"].([]any) var found []any @@ -755,8 +742,8 @@ func (p *Provider) batchGetPartition(params map[string]any) (*plugin.Response, e } func (p *Provider) batchUpdatePartition(params map[string]any) (*plugin.Response, error) { - dbName := strParam(params, "DatabaseName") - tableName := strParam(params, "TableName") + dbName := shared.StrParam(params, "DatabaseName") + tableName := shared.StrParam(params, "TableName") catID := catalogID(params) entries, _ := params["Entries"].([]any) var errs []any @@ -807,16 +794,16 @@ func partitionToMap(p *Partition) map[string]any { // ---- Crawler handlers ---- func (p *Provider) createCrawler(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } - role := strParam(params, "Role") - dbName := strParam(params, "DatabaseName") + role := shared.StrParam(params, "Role") + dbName := shared.StrParam(params, "DatabaseName") targets := marshalParam(params, "Targets") schedule := "" if s, ok := params["Schedule"].(map[string]any); ok { - schedule = strParam(s, "ScheduleExpression") + schedule = shared.StrParam(s, "ScheduleExpression") } else if s, ok := params["Schedule"].(string); ok { schedule = s } @@ -832,7 +819,7 @@ func (p *Provider) createCrawler(params map[string]any) (*plugin.Response, error } func (p *Provider) getCrawler(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") c, err := p.store.GetCrawler(name) if err != nil { if err == errCrawlerNotFound { @@ -856,12 +843,12 @@ func (p *Provider) getCrawlers(params map[string]any) (*plugin.Response, error) } func (p *Provider) updateCrawler(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } - role := strParam(params, "Role") - dbName := strParam(params, "DatabaseName") + role := shared.StrParam(params, "Role") + dbName := shared.StrParam(params, "DatabaseName") targets := marshalParam(params, "Targets") schedule := "" if s, ok := params["Schedule"].(string); ok { @@ -878,7 +865,7 @@ func (p *Provider) updateCrawler(params map[string]any) (*plugin.Response, error } func (p *Provider) deleteCrawler(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if err := p.store.DeleteCrawler(name); err != nil { if err == errCrawlerNotFound { return shared.JSONError("EntityNotFoundException", "Crawler not found", http.StatusBadRequest), nil @@ -889,7 +876,7 @@ func (p *Provider) deleteCrawler(params map[string]any) (*plugin.Response, error } func (p *Provider) startCrawler(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if err := p.store.UpdateCrawlerStatus(name, "RUNNING"); err != nil { if err == errCrawlerNotFound { return shared.JSONError("EntityNotFoundException", "Crawler not found", http.StatusBadRequest), nil @@ -900,7 +887,7 @@ func (p *Provider) startCrawler(params map[string]any) (*plugin.Response, error) } func (p *Provider) stopCrawler(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if err := p.store.UpdateCrawlerStatus(name, "READY"); err != nil { if err == errCrawlerNotFound { return shared.JSONError("EntityNotFoundException", "Crawler not found", http.StatusBadRequest), nil @@ -951,11 +938,11 @@ func crawlerToMap(c *Crawler) map[string]any { // ---- Job handlers ---- func (p *Provider) createJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } - role := strParam(params, "Role") + role := shared.StrParam(params, "Role") command := marshalParam(params, "Command") maxRetries := intParam(params, "MaxRetries", 0) timeout := intParam(params, "Timeout", 2880) @@ -971,7 +958,7 @@ func (p *Provider) createJob(params map[string]any) (*plugin.Response, error) { } func (p *Provider) getJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "JobName") + name := shared.StrParam(params, "JobName") j, err := p.store.GetJob(name) if err != nil { if err == errJobNotFound { @@ -995,7 +982,7 @@ func (p *Provider) getJobs(params map[string]any) (*plugin.Response, error) { } func (p *Provider) updateJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "JobName") + name := shared.StrParam(params, "JobName") if name == "" { return shared.JSONError("InvalidInputException", "JobName is required", http.StatusBadRequest), nil } @@ -1003,7 +990,7 @@ func (p *Provider) updateJob(params map[string]any) (*plugin.Response, error) { if input == nil { input = map[string]any{} } - role := strParam(input, "Role") + role := shared.StrParam(input, "Role") command := marshalParam(input, "Command") maxRetries := intParam(input, "MaxRetries", 0) timeout := intParam(input, "Timeout", 2880) @@ -1018,7 +1005,7 @@ func (p *Provider) updateJob(params map[string]any) (*plugin.Response, error) { } func (p *Provider) deleteJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "JobName") + name := shared.StrParam(params, "JobName") if err := p.store.DeleteJob(name); err != nil { if err == errJobNotFound { return shared.JSONError("EntityNotFoundException", "Job not found", http.StatusBadRequest), nil @@ -1050,7 +1037,7 @@ func (p *Provider) batchGetJobs(params map[string]any) (*plugin.Response, error) } func (p *Provider) startJobRun(params map[string]any) (*plugin.Response, error) { - jobName := strParam(params, "JobName") + jobName := shared.StrParam(params, "JobName") if jobName == "" { return shared.JSONError("InvalidInputException", "JobName is required", http.StatusBadRequest), nil } @@ -1063,7 +1050,7 @@ func (p *Provider) startJobRun(params map[string]any) (*plugin.Response, error) } func (p *Provider) getJobRun(params map[string]any) (*plugin.Response, error) { - runID := strParam(params, "RunId") + runID := shared.StrParam(params, "RunId") r, err := p.store.GetJobRun(runID) if err != nil { if err == errJobRunNotFound { @@ -1075,7 +1062,7 @@ func (p *Provider) getJobRun(params map[string]any) (*plugin.Response, error) { } func (p *Provider) getJobRuns(params map[string]any) (*plugin.Response, error) { - jobName := strParam(params, "JobName") + jobName := shared.StrParam(params, "JobName") runs, err := p.store.ListJobRuns(jobName) if err != nil { return nil, err @@ -1088,7 +1075,7 @@ func (p *Provider) getJobRuns(params map[string]any) (*plugin.Response, error) { } func (p *Provider) batchStopJobRun(params map[string]any) (*plugin.Response, error) { - jobName := strParam(params, "JobName") + jobName := shared.StrParam(params, "JobName") runIDs := stringsParam(params, "JobRunIds") var errs []any for _, id := range runIDs { @@ -1138,12 +1125,12 @@ func (p *Provider) createConnection(params map[string]any) (*plugin.Response, er if input == nil { return shared.JSONError("InvalidInputException", "ConnectionInput is required", http.StatusBadRequest), nil } - name := strParam(input, "Name") + name := shared.StrParam(input, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } catID := catalogID(params) - connType := strParam(input, "ConnectionType") + connType := shared.StrParam(input, "ConnectionType") if connType == "" { connType = "JDBC" } @@ -1159,7 +1146,7 @@ func (p *Provider) createConnection(params map[string]any) (*plugin.Response, er } func (p *Provider) getConnection(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") catID := catalogID(params) c, err := p.store.GetConnection(catID, name) if err != nil { @@ -1185,13 +1172,13 @@ func (p *Provider) getConnections(params map[string]any) (*plugin.Response, erro } func (p *Provider) updateConnection(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") catID := catalogID(params) input, _ := params["ConnectionInput"].(map[string]any) if input == nil { input = map[string]any{} } - connType := strParam(input, "ConnectionType") + connType := shared.StrParam(input, "ConnectionType") if connType == "" { connType = "JDBC" } @@ -1206,7 +1193,7 @@ func (p *Provider) updateConnection(params map[string]any) (*plugin.Response, er } func (p *Provider) deleteConnection(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ConnectionName") + name := shared.StrParam(params, "ConnectionName") catID := catalogID(params) if err := p.store.DeleteConnection(catID, name); err != nil { if err == errConnectionNotFound { @@ -1250,17 +1237,17 @@ func connectionToMap(c *Connection) map[string]any { // ---- Trigger handlers ---- func (p *Provider) createTrigger(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } - trigType := strParam(params, "Type") + trigType := shared.StrParam(params, "Type") if trigType == "" { trigType = "ON_DEMAND" } actions := marshalParamArray(params, "Actions") predicate := marshalParam(params, "Predicate") - schedule := strParam(params, "Schedule") + schedule := shared.StrParam(params, "Schedule") _, err := p.store.CreateTrigger(name, trigType, actions, predicate, schedule) if err != nil { if sqliteIsUnique(err) { @@ -1272,7 +1259,7 @@ func (p *Provider) createTrigger(params map[string]any) (*plugin.Response, error } func (p *Provider) getTrigger(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") t, err := p.store.GetTrigger(name) if err != nil { if err == errTriggerNotFound { @@ -1296,7 +1283,7 @@ func (p *Provider) getTriggers(params map[string]any) (*plugin.Response, error) } func (p *Provider) updateTrigger(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } @@ -1304,13 +1291,13 @@ func (p *Provider) updateTrigger(params map[string]any) (*plugin.Response, error if input == nil { input = map[string]any{} } - trigType := strParam(input, "Type") + trigType := shared.StrParam(input, "Type") if trigType == "" { trigType = "ON_DEMAND" } actions := marshalParamArray(input, "Actions") predicate := marshalParam(input, "Predicate") - schedule := strParam(input, "Schedule") + schedule := shared.StrParam(input, "Schedule") if err := p.store.UpdateTrigger(name, trigType, actions, predicate, schedule); err != nil { if err == errTriggerNotFound { return shared.JSONError("EntityNotFoundException", "Trigger not found", http.StatusBadRequest), nil @@ -1321,7 +1308,7 @@ func (p *Provider) updateTrigger(params map[string]any) (*plugin.Response, error } func (p *Provider) deleteTrigger(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if err := p.store.DeleteTrigger(name); err != nil { if err == errTriggerNotFound { return shared.JSONError("EntityNotFoundException", "Trigger not found", http.StatusBadRequest), nil @@ -1332,7 +1319,7 @@ func (p *Provider) deleteTrigger(params map[string]any) (*plugin.Response, error } func (p *Provider) startTrigger(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if err := p.store.UpdateTriggerState(name, "ACTIVATED"); err != nil { if err == errTriggerNotFound { return shared.JSONError("EntityNotFoundException", "Trigger not found", http.StatusBadRequest), nil @@ -1343,7 +1330,7 @@ func (p *Provider) startTrigger(params map[string]any) (*plugin.Response, error) } func (p *Provider) stopTrigger(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if err := p.store.UpdateTriggerState(name, "DEACTIVATED"); err != nil { if err == errTriggerNotFound { return shared.JSONError("EntityNotFoundException", "Trigger not found", http.StatusBadRequest), nil @@ -1391,7 +1378,7 @@ func triggerToMap(t *Trigger) map[string]any { // ---- SecurityConfiguration handlers ---- func (p *Provider) createSecurityConfiguration(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("InvalidInputException", "Name is required", http.StatusBadRequest), nil } @@ -1407,7 +1394,7 @@ func (p *Provider) createSecurityConfiguration(params map[string]any) (*plugin.R } func (p *Provider) getSecurityConfiguration(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") c, err := p.store.GetSecurityConfig(name) if err != nil { if err == errSecurityConfigNotFound { @@ -1431,7 +1418,7 @@ func (p *Provider) getSecurityConfigurations(params map[string]any) (*plugin.Res } func (p *Provider) deleteSecurityConfiguration(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if err := p.store.DeleteSecurityConfig(name); err != nil { if err == errSecurityConfigNotFound { return shared.JSONError("EntityNotFoundException", "SecurityConfiguration not found", http.StatusBadRequest), nil @@ -1454,7 +1441,7 @@ func securityConfigToMap(c *SecurityConfig) map[string]any { // ---- Tags handlers ---- func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") tagsRaw, _ := params["TagsToAdd"].(map[string]any) tags := make(map[string]string) for k, v := range tagsRaw { @@ -1469,7 +1456,7 @@ func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) } func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") keys := stringsParam(params, "TagsToRemove") if err := p.store.tags.RemoveTags(arn, keys); err != nil { return nil, err @@ -1478,7 +1465,7 @@ func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error } func (p *Provider) getTags(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") tags, err := p.store.tags.ListTags(arn) if err != nil { return nil, err diff --git a/internal/services/iam/provider.go b/internal/services/iam/provider.go index 8bd9a15b..13e3bfe7 100644 --- a/internal/services/iam/provider.go +++ b/internal/services/iam/provider.go @@ -259,11 +259,6 @@ func (p *IAMProvider) ListResources(ctx context.Context) ([]plugin.Resource, err return resources, nil } -// GetMetrics returns empty metrics. -func (p *IAMProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // Store returns the underlying IAMStore, allowing other providers (e.g. STS) // to share the same database instance. func (p *IAMProvider) Store() *IAMStore { diff --git a/internal/services/iam/sts_provider.go b/internal/services/iam/sts_provider.go index dcd94155..7b701492 100644 --- a/internal/services/iam/sts_provider.go +++ b/internal/services/iam/sts_provider.go @@ -106,11 +106,6 @@ func (p *STSProvider) ListResources(_ context.Context) ([]plugin.Resource, error return nil, nil } -// GetMetrics returns empty metrics. -func (p *STSProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- XML response structs --- type getCallerIdentityResponse struct { diff --git a/internal/services/identitystore/provider.go b/internal/services/identitystore/provider.go index 20d671e2..1cdaf57e 100644 --- a/internal/services/identitystore/provider.go +++ b/internal/services/identitystore/provider.go @@ -166,10 +166,6 @@ func (p *IdentityStoreProvider) ListResources(_ context.Context) ([]plugin.Resou return res, nil } -func (p *IdentityStoreProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- User handlers --- func (p *IdentityStoreProvider) createUser(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/iot/provider.go b/internal/services/iot/provider.go index f896f88d..f1666e9f 100644 --- a/internal/services/iot/provider.go +++ b/internal/services/iot/provider.go @@ -552,10 +552,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Thing operations --- func (p *Provider) createThing(params map[string]any) (*plugin.Response, error) { @@ -575,7 +571,7 @@ func (p *Provider) createThing(params map[string]any) (*plugin.Response, error) t := &Thing{ Name: name, ARN: arn, - TypeName: strParam(params, "thingTypeName"), + TypeName: shared.StrParam(params, "thingTypeName"), Attributes: attrsJSON, Version: 1, CreatedAt: time.Now().Unix(), @@ -1443,7 +1439,7 @@ func (p *Provider) createJob(id string, params map[string]any) (*plugin.Response Status: "IN_PROGRESS", Targets: targetsJSON, Document: docJSON, - Description: strParam(params, "description"), + Description: shared.StrParam(params, "description"), CreatedAt: time.Now().Unix(), } if err := p.store.CreateJob(j); err != nil { @@ -1828,11 +1824,6 @@ func extractPathParam(path, segment string) string { return "" } -func strParam(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func isUniqueErr(err error) bool { return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") } diff --git a/internal/services/iotdataplane/provider.go b/internal/services/iotdataplane/provider.go index dbc9be37..0f11ca1d 100644 --- a/internal/services/iotdataplane/provider.go +++ b/internal/services/iotdataplane/provider.go @@ -119,10 +119,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return []plugin.Resource{}, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- ThingShadow --- func (p *Provider) getThingShadow(thingName, shadowName string) (*plugin.Response, error) { diff --git a/internal/services/iotwireless/provider.go b/internal/services/iotwireless/provider.go index b43abf53..b42aec8f 100644 --- a/internal/services/iotwireless/provider.go +++ b/internal/services/iotwireless/provider.go @@ -210,10 +210,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Destination operations --- func (p *Provider) createDestination(params map[string]any) (*plugin.Response, error) { @@ -225,10 +221,10 @@ func (p *Provider) createDestination(params map[string]any) (*plugin.Response, e d := &Destination{ Name: name, ARN: arn, - Expression: strParam(params, "Expression"), + Expression: shared.StrParam(params, "Expression"), ExpressionType: strParamDefault(params, "ExpressionType", "RuleName"), - RoleARN: strParam(params, "RoleArn"), - Description: strParam(params, "Description"), + RoleARN: shared.StrParam(params, "RoleArn"), + Description: shared.StrParam(params, "Description"), } if err := p.store.CreateDestination(d); err != nil { if isUniqueErr(err) { @@ -302,7 +298,7 @@ func (p *Provider) createDeviceProfile(params map[string]any) (*plugin.Response, dp := &DeviceProfile{ ID: id, ARN: arn, - Name: strParam(params, "Name"), + Name: shared.StrParam(params, "Name"), Config: configJSON, } if err := p.store.CreateDeviceProfile(dp); err != nil { @@ -363,7 +359,7 @@ func (p *Provider) createServiceProfile(params map[string]any) (*plugin.Response sp := &ServiceProfile{ ID: id, ARN: arn, - Name: strParam(params, "Name"), + Name: shared.StrParam(params, "Name"), Config: configJSON, } if err := p.store.CreateServiceProfile(sp); err != nil { @@ -424,10 +420,10 @@ func (p *Provider) createWirelessDevice(params map[string]any) (*plugin.Response wd := &WirelessDevice{ ID: id, ARN: arn, - Name: strParam(params, "Name"), + Name: shared.StrParam(params, "Name"), Type: strParamDefault(params, "Type", "LoRaWAN"), - Destination: strParam(params, "DestinationName"), - Description: strParam(params, "Description"), + Destination: shared.StrParam(params, "DestinationName"), + Description: shared.StrParam(params, "Description"), Config: configJSON, } if err := p.store.CreateWirelessDevice(wd); err != nil { @@ -504,8 +500,8 @@ func (p *Provider) createWirelessGateway(params map[string]any) (*plugin.Respons wg := &WirelessGateway{ ID: id, ARN: arn, - Name: strParam(params, "Name"), - Description: strParam(params, "Description"), + Name: shared.StrParam(params, "Name"), + Description: shared.StrParam(params, "Description"), Config: configJSON, } if err := p.store.CreateWirelessGateway(wg); err != nil { @@ -580,7 +576,7 @@ func (p *Provider) createFuotaTask(params map[string]any) (*plugin.Response, err ft := &FuotaTask{ ID: id, ARN: arn, - Name: strParam(params, "Name"), + Name: shared.StrParam(params, "Name"), Status: "Pending", Config: configJSON, } @@ -667,7 +663,7 @@ func (p *Provider) createMulticastGroup(params map[string]any) (*plugin.Response mg := &MulticastGroup{ ID: id, ARN: arn, - Name: strParam(params, "Name"), + Name: shared.StrParam(params, "Name"), Status: "Active", Config: configJSON, } @@ -1002,11 +998,6 @@ func extractPathParam(path, segment string) string { return "" } -func strParam(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func strParamDefault(params map[string]any, key, def string) string { v, _ := params[key].(string) if v == "" { diff --git a/internal/services/kafka/provider.go b/internal/services/kafka/provider.go index d45fc46a..14a9b7e6 100644 --- a/internal/services/kafka/provider.go +++ b/internal/services/kafka/provider.go @@ -12,6 +12,7 @@ import ( "net/http" "path/filepath" "strings" + "unicode" "github.com/skyoo2003/devcloud/internal/plugin" "github.com/skyoo2003/devcloud/internal/shared" @@ -52,8 +53,7 @@ func (p *Provider) HandleRequest(ctx context.Context, op string, req *http.Reque if resp != nil && resp.StatusCode < 300 && len(resp.Body) > 0 { var data any if json.Unmarshal(resp.Body, &data) == nil { - converted := shared.CamelCaseKeys(data) - if b, err := json.Marshal(converted); err == nil { + if b, err := json.Marshal(mapKeys(data, toCamelCase)); err == nil { resp.Body = b } } @@ -76,7 +76,7 @@ func (p *Provider) handleRequest(_ context.Context, op string, req *http.Request } // Normalize camelCase input params to PascalCase for internal consistency. - params = shared.PascalCaseKeys(params) + params, _ = mapKeys(params, toPascalCase).(map[string]any) // Extract path parameters from URL path path := req.URL.Path @@ -443,10 +443,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ── Cluster operations ───────────────────────────────────────────────────── func (p *Provider) createCluster(params map[string]any) (*plugin.Response, error) { @@ -1343,6 +1339,63 @@ func isUniqueErr(err error) bool { return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") } +// mapKeys recursively rewrites every map key in v with f. Kafka is the only +// service with a camelCase wire format, so the casing walk lives here. +func mapKeys(v any, f func(string) string) any { + switch val := v.(type) { + case map[string]any: + out := make(map[string]any, len(val)) + for k, item := range val { + out[f(k)] = mapKeys(item, f) + } + return out + case []any: + out := make([]any, len(val)) + for i, item := range val { + out[i] = mapKeys(item, f) + } + return out + case []map[string]any: + out := make([]any, len(val)) + for i, item := range val { + out[i] = mapKeys(item, f) + } + return out + default: + return v + } +} + +// toCamelCase lowercases the leading uppercase run of a PascalCase name: +// "ClusterArn" → "clusterArn", "ARN" → "arn", "ARNPrefix" → "arnPrefix". +func toCamelCase(s string) string { + runes := []rune(s) + i := 0 + for i < len(runes) && unicode.IsUpper(runes[i]) { + i++ + } + switch { + case i == 0: // already camelCase + return s + case i == len(runes): // all caps + return strings.ToLower(s) + case i == 1: + return strings.ToLower(string(runes[0])) + string(runes[1:]) + default: + // Last uppercase letter starts the next word: "ARNPrefix" → "arnPrefix". + return strings.ToLower(string(runes[:i-1])) + string(runes[i-1:]) + } +} + +func toPascalCase(s string) string { + if s == "" { + return s + } + runes := []rune(s) + runes[0] = unicode.ToUpper(runes[0]) + return string(runes) +} + func init() { plugin.DefaultRegistry.Register("kafka", func() plugin.ServicePlugin { return &Provider{} diff --git a/internal/services/kafka/provider_test.go b/internal/services/kafka/provider_test.go index 55b0f5ad..43790ae1 100644 --- a/internal/services/kafka/provider_test.go +++ b/internal/services/kafka/provider_test.go @@ -300,3 +300,32 @@ func TestGetBootstrapBrokers(t *testing.T) { resp2 := callOP(t, p, "GET", "/v1/clusters/nonexistent/bootstrap-brokers", "GetBootstrapBrokers", "") assert.Equal(t, 404, resp2.StatusCode) } + +func TestMapKeys_RoundTrip(t *testing.T) { + in := map[string]any{ + "ClusterArn": "arn", + "ARN": "x", + "ARNPrefix": "y", + "BrokerNodeGroupInfo": map[string]any{ + "ClientSubnets": []any{"a", "b"}, + }, + "ClusterInfoList": []any{ + map[string]any{"ClusterName": "c1"}, + }, + } + + camel, ok := mapKeys(in, toCamelCase).(map[string]any) + require.True(t, ok) + assert.Equal(t, "arn", camel["clusterArn"]) + assert.Equal(t, "x", camel["arn"]) + assert.Equal(t, "y", camel["arnPrefix"]) + assert.Contains(t, camel["brokerNodeGroupInfo"], "clientSubnets") + assert.Equal(t, "c1", camel["clusterInfoList"].([]any)[0].(map[string]any)["clusterName"]) + + // PascalCase only touches the first rune, so it is the inverse for keys that + // start with a single uppercase letter. + back, ok := mapKeys(camel, toPascalCase).(map[string]any) + require.True(t, ok) + assert.Equal(t, "arn", back["ClusterArn"]) + assert.Equal(t, "c1", back["ClusterInfoList"].([]any)[0].(map[string]any)["ClusterName"]) +} diff --git a/internal/services/kinesis/provider.go b/internal/services/kinesis/provider.go index b0f0f7f3..72f4f275 100644 --- a/internal/services/kinesis/provider.go +++ b/internal/services/kinesis/provider.go @@ -188,10 +188,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Stream CRUD --- func (p *Provider) createStream(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/kinesisanalyticsv2/provider.go b/internal/services/kinesisanalyticsv2/provider.go index 2609a13a..b5c9f517 100644 --- a/internal/services/kinesisanalyticsv2/provider.go +++ b/internal/services/kinesisanalyticsv2/provider.go @@ -138,10 +138,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func appToMap(app *Application) map[string]any { diff --git a/internal/services/kms/provider.go b/internal/services/kms/provider.go index e9fb3beb..5b17ba71 100644 --- a/internal/services/kms/provider.go +++ b/internal/services/kms/provider.go @@ -15,10 +15,12 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID -const defaultRegion = "us-east-1" +const defaultRegion = shared.DefaultRegion type Provider struct { store *KMSStore @@ -144,10 +146,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return resources, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) createKey(params map[string]any) (*plugin.Response, error) { description, _ := params["Description"].(string) keySpec, _ := params["KeySpec"].(string) diff --git a/internal/services/lakeformation/provider.go b/internal/services/lakeformation/provider.go index 130b875f..6fa97738 100644 --- a/internal/services/lakeformation/provider.go +++ b/internal/services/lakeformation/provider.go @@ -229,14 +229,10 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- LFTag --- func (p *Provider) createLFTag(params map[string]any) (*plugin.Response, error) { - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) key, _ := params["TagKey"].(string) if key == "" { return shared.JSONError("ValidationException", "TagKey is required", http.StatusBadRequest), nil @@ -254,7 +250,7 @@ func (p *Provider) createLFTag(params map[string]any) (*plugin.Response, error) } func (p *Provider) getLFTag(params map[string]any) (*plugin.Response, error) { - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) key, _ := params["TagKey"].(string) if key == "" { return shared.JSONError("ValidationException", "TagKey is required", http.StatusBadRequest), nil @@ -273,7 +269,7 @@ func (p *Provider) getLFTag(params map[string]any) (*plugin.Response, error) { } func (p *Provider) updateLFTag(params map[string]any) (*plugin.Response, error) { - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) key, _ := params["TagKey"].(string) if key == "" { return shared.JSONError("ValidationException", "TagKey is required", http.StatusBadRequest), nil @@ -311,7 +307,7 @@ func (p *Provider) updateLFTag(params map[string]any) (*plugin.Response, error) } func (p *Provider) deleteLFTag(params map[string]any) (*plugin.Response, error) { - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) key, _ := params["TagKey"].(string) if key == "" { return shared.JSONError("ValidationException", "TagKey is required", http.StatusBadRequest), nil @@ -323,7 +319,7 @@ func (p *Provider) deleteLFTag(params map[string]any) (*plugin.Response, error) } func (p *Provider) listLFTags(params map[string]any) (*plugin.Response, error) { - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) tags, err := p.store.ListLFTags(catalogID) if err != nil { return nil, err @@ -351,7 +347,7 @@ func (p *Provider) createLFTagExpression(params map[string]any) (*plugin.Respons if name == "" { return shared.JSONError("ValidationException", "Name is required", http.StatusBadRequest), nil } - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) description, _ := params["Description"].(string) rawExpr, _ := params["Expression"].([]any) exprJSON := marshalJSON(rawExpr) @@ -414,7 +410,7 @@ func (p *Provider) deleteLFTagExpression(params map[string]any) (*plugin.Respons } func (p *Provider) listLFTagExpressions(params map[string]any) (*plugin.Response, error) { - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) exprs, err := p.store.ListLFTagExpressions(catalogID) if err != nil { return nil, err @@ -653,7 +649,7 @@ func (p *Provider) listResources(_ map[string]any) (*plugin.Response, error) { // --- DataLakeSettings --- func (p *Provider) getDataLakeSettings(params map[string]any) (*plugin.Response, error) { - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) st, err := p.store.GetSettings(catalogID) if err != nil { return nil, err @@ -677,7 +673,7 @@ func (p *Provider) getDataLakeSettings(params map[string]any) (*plugin.Response, } func (p *Provider) putDataLakeSettings(params map[string]any) (*plugin.Response, error) { - catalogID := strParam(params, "CatalogId", "000000000000") + catalogID := strParam(params, "CatalogId", plugin.DefaultAccountID) settingsRaw, _ := params["DataLakeSettings"].(map[string]any) if settingsRaw == nil { settingsRaw = map[string]any{} diff --git a/internal/services/lambda/provider.go b/internal/services/lambda/provider.go index 1895c5cb..a5b876ec 100644 --- a/internal/services/lambda/provider.go +++ b/internal/services/lambda/provider.go @@ -17,11 +17,13 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const ( defaultAccountID = plugin.DefaultAccountID - defaultRegion = "us-east-1" + defaultRegion = shared.DefaultRegion lambdaAPIPrefix = "/2015-03-31/functions" lambdaTagsPrefix = "/2017-03-31/tags/" lambdaESMPrefix = "/2015-03-31/event-source-mappings" @@ -245,15 +247,6 @@ func (p *LambdaProvider) ListResources(_ context.Context) ([]plugin.Resource, er return resources, nil } -// GetMetrics returns basic metrics for the Lambda service. -func (p *LambdaProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - functions, err := p.store.ListFunctions(defaultAccountID) - if err != nil { - return nil, err - } - return &plugin.ServiceMetrics{ResourceCount: len(functions)}, nil -} - // --- operation implementations --- // createFunctionRequest is the JSON body sent by boto3 for CreateFunction. diff --git a/internal/services/managedblockchain/provider.go b/internal/services/managedblockchain/provider.go index 0ea45451..1eaaf338 100644 --- a/internal/services/managedblockchain/provider.go +++ b/internal/services/managedblockchain/provider.go @@ -294,10 +294,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Networks --- func (p *Provider) createNetwork(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/mediaconvert/provider.go b/internal/services/mediaconvert/provider.go index 65147999..a4f7f573 100644 --- a/internal/services/mediaconvert/provider.go +++ b/internal/services/mediaconvert/provider.go @@ -188,11 +188,6 @@ func (p *MediaConvertProvider) ListResources(_ context.Context) ([]plugin.Resour return res, nil } -// GetMetrics returns empty metrics. -func (p *MediaConvertProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Job handlers --- func (p *MediaConvertProvider) handleCreateJob(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/mediaconvert/store.go b/internal/services/mediaconvert/store.go index 281d178c..b33f7e36 100644 --- a/internal/services/mediaconvert/store.go +++ b/internal/services/mediaconvert/store.go @@ -11,9 +11,11 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/storage/sqlite" + + "github.com/skyoo2003/devcloud/internal/shared" ) -const region = "us-east-1" +const region = shared.DefaultRegion var ( errJobTemplateNotFound = errors.New("job template not found") diff --git a/internal/services/memorydb/provider.go b/internal/services/memorydb/provider.go index b503af69..740234c6 100644 --- a/internal/services/memorydb/provider.go +++ b/internal/services/memorydb/provider.go @@ -188,10 +188,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Cluster handlers ---- func (p *Provider) createCluster(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/mq/provider.go b/internal/services/mq/provider.go index e3a4aea3..16b1b3a1 100644 --- a/internal/services/mq/provider.go +++ b/internal/services/mq/provider.go @@ -263,10 +263,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Broker ---- func (p *Provider) createBroker(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/mwaa/provider.go b/internal/services/mwaa/provider.go index e0a41e4b..a2a4b138 100644 --- a/internal/services/mwaa/provider.go +++ b/internal/services/mwaa/provider.go @@ -326,10 +326,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Environment CRUD --- func (p *Provider) createEnvironment(name string, params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/neptune/provider.go b/internal/services/neptune/provider.go index 0bdafebe..1427d5e5 100644 --- a/internal/services/neptune/provider.go +++ b/internal/services/neptune/provider.go @@ -244,10 +244,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func dbError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/opensearch/provider.go b/internal/services/opensearch/provider.go index 1f0f76ca..f9fda236 100644 --- a/internal/services/opensearch/provider.go +++ b/internal/services/opensearch/provider.go @@ -269,10 +269,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Domain CRUD --- func (p *Provider) createDomain(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/organizations/provider.go b/internal/services/organizations/provider.go index 64f22cb2..a97f71c7 100644 --- a/internal/services/organizations/provider.go +++ b/internal/services/organizations/provider.go @@ -168,10 +168,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Organization ---- func (p *Provider) createOrganization(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/pinpoint/provider.go b/internal/services/pinpoint/provider.go index 34a680f8..9321cf40 100644 --- a/internal/services/pinpoint/provider.go +++ b/internal/services/pinpoint/provider.go @@ -47,10 +47,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return []plugin.Resource{}, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request) (*plugin.Response, error) { body, err := io.ReadAll(req.Body) if err != nil { @@ -361,9 +357,9 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // ── App handlers ────────────────────────────────────────────────────────────── func (p *Provider) createApp(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { - name = strParam(params, "name") + name = shared.StrParam(params, "name") } if name == "" { return shared.JSONError("BadRequestException", "Name is required", http.StatusBadRequest), nil @@ -420,12 +416,12 @@ func appToMap(a *App) map[string]any { // ── Campaign handlers ───────────────────────────────────────────────────────── func (p *Provider) createCampaign(appID string, params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("BadRequestException", "Name is required", http.StatusBadRequest), nil } - description := strParam(params, "Description") - segmentID := strParam(params, "SegmentId") + description := shared.StrParam(params, "Description") + segmentID := shared.StrParam(params, "SegmentId") id := generateID() arn := fmt.Sprintf("arn:aws:mobiletargeting:us-east-1:000000000000:apps/%s/campaigns/%s", appID, id) c, err := p.store.CreateCampaign(id, appID, arn, name, description, segmentID) @@ -456,9 +452,9 @@ func (p *Provider) getCampaigns(appID string) (*plugin.Response, error) { } func (p *Provider) updateCampaign(appID, campaignID string, params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") - description := strParam(params, "Description") - segmentID := strParam(params, "SegmentId") + name := shared.StrParam(params, "Name") + description := shared.StrParam(params, "Description") + segmentID := shared.StrParam(params, "SegmentId") c, err := p.store.UpdateCampaign(appID, campaignID, name, description, segmentID) if err != nil { return shared.JSONError("NotFoundException", "Campaign not found", http.StatusNotFound), nil @@ -517,11 +513,11 @@ func campaignToMap(c *Campaign) map[string]any { // ── Segment handlers ────────────────────────────────────────────────────────── func (p *Provider) createSegment(appID string, params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("BadRequestException", "Name is required", http.StatusBadRequest), nil } - segType := strParam(params, "SegmentType") + segType := shared.StrParam(params, "SegmentType") if segType == "" { segType = "DIMENSIONAL" } @@ -555,7 +551,7 @@ func (p *Provider) getSegments(appID string) (*plugin.Response, error) { } func (p *Provider) updateSegment(appID, segmentID string, params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") seg, err := p.store.UpdateSegment(appID, segmentID, name) if err != nil { return shared.JSONError("NotFoundException", "Segment not found", http.StatusNotFound), nil @@ -605,7 +601,7 @@ func segmentToMap(seg *Segment) map[string]any { // ── Journey handlers ────────────────────────────────────────────────────────── func (p *Provider) createJourney(appID string, params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("BadRequestException", "Name is required", http.StatusBadRequest), nil } @@ -639,7 +635,7 @@ func (p *Provider) listJourneys(appID string) (*plugin.Response, error) { } func (p *Provider) updateJourney(appID, journeyID string, params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") j, err := p.store.UpdateJourney(appID, journeyID, name) if err != nil { return shared.JSONError("NotFoundException", "Journey not found", http.StatusNotFound), nil @@ -648,7 +644,7 @@ func (p *Provider) updateJourney(appID, journeyID string, params map[string]any) } func (p *Provider) updateJourneyState(appID, journeyID string, params map[string]any) (*plugin.Response, error) { - state := strParam(params, "State") + state := shared.StrParam(params, "State") if state == "" { state = "ACTIVE" } @@ -685,16 +681,16 @@ func journeyToMap(j *Journey) map[string]any { // ── Template handlers ───────────────────────────────────────────────────────── func (p *Provider) createTemplate(name, tType string, params map[string]any) (*plugin.Response, error) { - subject := strParam(params, "Subject") - htmlBody := strParam(params, "HtmlPart") + subject := shared.StrParam(params, "Subject") + htmlBody := shared.StrParam(params, "HtmlPart") if htmlBody == "" { - htmlBody = strParam(params, "HtmlBody") + htmlBody = shared.StrParam(params, "HtmlBody") } - textBody := strParam(params, "TextPart") + textBody := shared.StrParam(params, "TextPart") if textBody == "" { - textBody = strParam(params, "TextBody") + textBody = shared.StrParam(params, "TextBody") } - body := strParam(params, "Body") + body := shared.StrParam(params, "Body") _, err := p.store.CreateTemplate(name, tType, subject, htmlBody, textBody, body) if err != nil { return shared.JSONError("InternalServerErrorException", err.Error(), http.StatusInternalServerError), nil @@ -714,16 +710,16 @@ func (p *Provider) getTemplate(name, tType string) (*plugin.Response, error) { } func (p *Provider) updateTemplate(name, tType string, params map[string]any) (*plugin.Response, error) { - subject := strParam(params, "Subject") - htmlBody := strParam(params, "HtmlPart") + subject := shared.StrParam(params, "Subject") + htmlBody := shared.StrParam(params, "HtmlPart") if htmlBody == "" { - htmlBody = strParam(params, "HtmlBody") + htmlBody = shared.StrParam(params, "HtmlBody") } - textBody := strParam(params, "TextPart") + textBody := shared.StrParam(params, "TextPart") if textBody == "" { - textBody = strParam(params, "TextBody") + textBody = shared.StrParam(params, "TextBody") } - body := strParam(params, "Body") + body := shared.StrParam(params, "Body") _, err := p.store.UpdateTemplate(name, tType, subject, htmlBody, textBody, body) if err != nil { return shared.JSONError("NotFoundException", "Template not found", http.StatusNotFound), nil @@ -1014,15 +1010,6 @@ func tagARN(path string) string { // ── Misc helpers ────────────────────────────────────────────────────────────── -func strParam(params map[string]any, key string) string { - if v, ok := params[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func extractTags(params map[string]any) map[string]string { tags := map[string]string{} for _, key := range []string{"tags", "Tags"} { diff --git a/internal/services/pipes/provider.go b/internal/services/pipes/provider.go index 06574990..c2d2c823 100644 --- a/internal/services/pipes/provider.go +++ b/internal/services/pipes/provider.go @@ -140,10 +140,6 @@ func (p *PipesProvider) ListResources(_ context.Context) ([]plugin.Resource, err return res, nil } -func (p *PipesProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Operations --- func (p *PipesProvider) handleCreatePipe(req *http.Request) (*plugin.Response, error) { diff --git a/internal/services/ram/provider.go b/internal/services/ram/provider.go index 678cab56..3631a833 100644 --- a/internal/services/ram/provider.go +++ b/internal/services/ram/provider.go @@ -216,10 +216,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- ResourceShare CRUD --- func (p *Provider) createResourceShare(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/rds/provider.go b/internal/services/rds/provider.go index 3259b56a..21c3103b 100644 --- a/internal/services/rds/provider.go +++ b/internal/services/rds/provider.go @@ -220,10 +220,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func rdsError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/redshift/provider.go b/internal/services/redshift/provider.go index 8351e4f5..d91a76ac 100644 --- a/internal/services/redshift/provider.go +++ b/internal/services/redshift/provider.go @@ -154,10 +154,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func rsError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/resourcegroups/provider.go b/internal/services/resourcegroups/provider.go index a8a38b8d..b8c6394d 100644 --- a/internal/services/resourcegroups/provider.go +++ b/internal/services/resourcegroups/provider.go @@ -124,10 +124,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- CRUD --- func (p *Provider) createGroup(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/resourcegroupstaggingapi/provider.go b/internal/services/resourcegroupstaggingapi/provider.go index 086ece1c..6cde4192 100644 --- a/internal/services/resourcegroupstaggingapi/provider.go +++ b/internal/services/resourcegroupstaggingapi/provider.go @@ -144,10 +144,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return []plugin.Resource{}, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) getResources(params map[string]any) (*plugin.Response, error) { // Parse TagFilters var tagFilters []TagFilter diff --git a/internal/services/route53/provider.go b/internal/services/route53/provider.go index 732d5534..ee6ebd52 100644 --- a/internal/services/route53/provider.go +++ b/internal/services/route53/provider.go @@ -179,10 +179,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func zoneIDFromPath(path string) string { diff --git a/internal/services/route53resolver/provider.go b/internal/services/route53resolver/provider.go index 6055784c..d6540484 100644 --- a/internal/services/route53resolver/provider.go +++ b/internal/services/route53resolver/provider.go @@ -235,17 +235,8 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ==================== helpers ==================== -func str(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func intParam(params map[string]any, key string) int { switch v := params[key].(type) { case float64: @@ -399,8 +390,8 @@ func fwRuleGroupAssocToMap(r *fwRuleGroupAssocRow) map[string]any { // ==================== ResolverEndpoint ==================== func (p *Provider) createResolverEndpoint(params map[string]any) (*plugin.Response, error) { - name := str(params, "Name") - direction := str(params, "Direction") + name := shared.StrParam(params, "Name") + direction := shared.StrParam(params, "Direction") if direction == "" { direction = "INBOUND" } @@ -449,7 +440,7 @@ func (p *Provider) createResolverEndpoint(params map[string]any) (*plugin.Respon } func (p *Provider) getResolverEndpoint(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverEndpointId") + id := shared.StrParam(params, "ResolverEndpointId") if id == "" { return shared.JSONError("ValidationException", "ResolverEndpointId is required", http.StatusBadRequest), nil } @@ -476,8 +467,8 @@ func (p *Provider) listResolverEndpoints(_ map[string]any) (*plugin.Response, er } func (p *Provider) updateResolverEndpoint(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverEndpointId") - name := str(params, "Name") + id := shared.StrParam(params, "ResolverEndpointId") + name := shared.StrParam(params, "Name") if err := p.store.UpdateEndpoint(id, name); err != nil { return shared.JSONError("ResourceNotFoundException", "resolver endpoint not found", http.StatusBadRequest), nil } @@ -486,7 +477,7 @@ func (p *Provider) updateResolverEndpoint(params map[string]any) (*plugin.Respon } func (p *Provider) deleteResolverEndpoint(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverEndpointId") + id := shared.StrParam(params, "ResolverEndpointId") ep, err := p.store.GetEndpoint(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "resolver endpoint not found", http.StatusBadRequest), nil @@ -499,7 +490,7 @@ func (p *Provider) deleteResolverEndpoint(params map[string]any) (*plugin.Respon } func (p *Provider) associateResolverEndpointIpAddress(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverEndpointId") + id := shared.StrParam(params, "ResolverEndpointId") ipMap, _ := params["IpAddress"].(map[string]any) subnet, _ := ipMap["SubnetId"].(string) ip, _ := ipMap["Ip"].(string) @@ -517,7 +508,7 @@ func (p *Provider) associateResolverEndpointIpAddress(params map[string]any) (*p } func (p *Provider) disassociateResolverEndpointIpAddress(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverEndpointId") + id := shared.StrParam(params, "ResolverEndpointId") ipMap, _ := params["IpAddress"].(map[string]any) ipID, _ := ipMap["IpId"].(string) ep, err := p.store.DisassociateEndpointIP(id, ipID) @@ -528,7 +519,7 @@ func (p *Provider) disassociateResolverEndpointIpAddress(params map[string]any) } func (p *Provider) listResolverEndpointIpAddresses(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverEndpointId") + id := shared.StrParam(params, "ResolverEndpointId") ips, err := p.store.ListEndpointIPs(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "resolver endpoint not found", http.StatusBadRequest), nil @@ -551,16 +542,16 @@ func (p *Provider) listResolverEndpointIpAddresses(params map[string]any) (*plug // ==================== ResolverRule ==================== func (p *Provider) createResolverRule(params map[string]any) (*plugin.Response, error) { - name := str(params, "Name") - domainName := str(params, "DomainName") + name := shared.StrParam(params, "Name") + domainName := shared.StrParam(params, "DomainName") if domainName != "" && !strings.HasSuffix(domainName, ".") { domainName += "." } - ruleType := str(params, "RuleType") + ruleType := shared.StrParam(params, "RuleType") if ruleType == "" { ruleType = "FORWARD" } - endpointID := str(params, "ResolverEndpointId") + endpointID := shared.StrParam(params, "ResolverEndpointId") var targetIPs []map[string]any if raw, ok := params["TargetIps"].([]any); ok { @@ -585,7 +576,7 @@ func (p *Provider) createResolverRule(params map[string]any) (*plugin.Response, } func (p *Provider) getResolverRule(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverRuleId") + id := shared.StrParam(params, "ResolverRuleId") rule, err := p.store.GetRule(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "resolver rule not found", http.StatusBadRequest), nil @@ -609,7 +600,7 @@ func (p *Provider) listResolverRules(_ map[string]any) (*plugin.Response, error) } func (p *Provider) updateResolverRule(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverRuleId") + id := shared.StrParam(params, "ResolverRuleId") cfg, _ := params["Config"].(map[string]any) name, _ := cfg["Name"].(string) endpointID, _ := cfg["ResolverEndpointId"].(string) @@ -642,7 +633,7 @@ func (p *Provider) updateResolverRule(params map[string]any) (*plugin.Response, } func (p *Provider) deleteResolverRule(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverRuleId") + id := shared.StrParam(params, "ResolverRuleId") rule, err := p.store.GetRule(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "resolver rule not found", http.StatusBadRequest), nil @@ -655,9 +646,9 @@ func (p *Provider) deleteResolverRule(params map[string]any) (*plugin.Response, } func (p *Provider) associateResolverRule(params map[string]any) (*plugin.Response, error) { - ruleID := str(params, "ResolverRuleId") - vpcID := str(params, "VPCId") - name := str(params, "Name") + ruleID := shared.StrParam(params, "ResolverRuleId") + vpcID := shared.StrParam(params, "VPCId") + name := shared.StrParam(params, "Name") id := shared.GenerateID("rslvr-ra-", 24) assoc, err := p.store.AssociateRule(id, ruleID, vpcID, name) if err != nil { @@ -667,8 +658,8 @@ func (p *Provider) associateResolverRule(params map[string]any) (*plugin.Respons } func (p *Provider) disassociateResolverRule(params map[string]any) (*plugin.Response, error) { - ruleID := str(params, "ResolverRuleId") - vpcID := str(params, "VPCId") + ruleID := shared.StrParam(params, "ResolverRuleId") + vpcID := shared.StrParam(params, "VPCId") assoc, err := p.store.DisassociateRule(ruleID, vpcID) if err != nil { return shared.JSONError("ResourceNotFoundException", "rule association not found", http.StatusBadRequest), nil @@ -677,7 +668,7 @@ func (p *Provider) disassociateResolverRule(params map[string]any) (*plugin.Resp } func (p *Provider) getResolverRuleAssociation(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverRuleAssociationId") + id := shared.StrParam(params, "ResolverRuleAssociationId") assoc, err := p.store.GetRuleAssociation(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "rule association not found", http.StatusBadRequest), nil @@ -711,8 +702,8 @@ func (p *Provider) putResolverRulePolicy(_ map[string]any) (*plugin.Response, er // ==================== QueryLogConfig ==================== func (p *Provider) createResolverQueryLogConfig(params map[string]any) (*plugin.Response, error) { - name := str(params, "Name") - dest := str(params, "DestinationArn") + name := shared.StrParam(params, "Name") + dest := shared.StrParam(params, "DestinationArn") if name == "" { return shared.JSONError("ValidationException", "Name is required", http.StatusBadRequest), nil } @@ -729,7 +720,7 @@ func (p *Provider) createResolverQueryLogConfig(params map[string]any) (*plugin. } func (p *Provider) getResolverQueryLogConfig(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverQueryLogConfigId") + id := shared.StrParam(params, "ResolverQueryLogConfigId") cfg, err := p.store.GetQueryLogConfig(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "query log config not found", http.StatusBadRequest), nil @@ -754,7 +745,7 @@ func (p *Provider) listResolverQueryLogConfigs(_ map[string]any) (*plugin.Respon } func (p *Provider) deleteResolverQueryLogConfig(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverQueryLogConfigId") + id := shared.StrParam(params, "ResolverQueryLogConfigId") cfg, err := p.store.GetQueryLogConfig(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "query log config not found", http.StatusBadRequest), nil @@ -767,8 +758,8 @@ func (p *Provider) deleteResolverQueryLogConfig(params map[string]any) (*plugin. } func (p *Provider) associateResolverQueryLogConfig(params map[string]any) (*plugin.Response, error) { - configID := str(params, "ResolverQueryLogConfigId") - resourceID := str(params, "ResourceId") + configID := shared.StrParam(params, "ResolverQueryLogConfigId") + resourceID := shared.StrParam(params, "ResourceId") id := shared.GenerateID("rqlca-", 28) assoc, err := p.store.AssociateQueryLogConfig(id, configID, resourceID) if err != nil { @@ -778,8 +769,8 @@ func (p *Provider) associateResolverQueryLogConfig(params map[string]any) (*plug } func (p *Provider) disassociateResolverQueryLogConfig(params map[string]any) (*plugin.Response, error) { - configID := str(params, "ResolverQueryLogConfigId") - resourceID := str(params, "ResourceId") + configID := shared.StrParam(params, "ResolverQueryLogConfigId") + resourceID := shared.StrParam(params, "ResourceId") assoc, err := p.store.DisassociateQueryLogConfig(configID, resourceID) if err != nil { return shared.JSONError("ResourceNotFoundException", "query log association not found", http.StatusBadRequest), nil @@ -788,7 +779,7 @@ func (p *Provider) disassociateResolverQueryLogConfig(params map[string]any) (*p } func (p *Provider) getResolverQueryLogConfigAssociation(params map[string]any) (*plugin.Response, error) { - id := str(params, "ResolverQueryLogConfigAssociationId") + id := shared.StrParam(params, "ResolverQueryLogConfigAssociationId") assoc, err := p.store.GetQueryLogConfigAssociation(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "query log association not found", http.StatusBadRequest), nil @@ -823,7 +814,7 @@ func (p *Provider) putResolverQueryLogConfigPolicy(_ map[string]any) (*plugin.Re // ==================== FirewallRuleGroup ==================== func (p *Provider) createFirewallRuleGroup(params map[string]any) (*plugin.Response, error) { - name := str(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("ValidationException", "Name is required", http.StatusBadRequest), nil } @@ -840,7 +831,7 @@ func (p *Provider) createFirewallRuleGroup(params map[string]any) (*plugin.Respo } func (p *Provider) getFirewallRuleGroup(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallRuleGroupId") + id := shared.StrParam(params, "FirewallRuleGroupId") grp, err := p.store.GetFirewallRuleGroup(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "firewall rule group not found", http.StatusBadRequest), nil @@ -865,7 +856,7 @@ func (p *Provider) listFirewallRuleGroups(_ map[string]any) (*plugin.Response, e } func (p *Provider) deleteFirewallRuleGroup(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallRuleGroupId") + id := shared.StrParam(params, "FirewallRuleGroupId") grp, err := p.store.GetFirewallRuleGroup(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "firewall rule group not found", http.StatusBadRequest), nil @@ -880,15 +871,15 @@ func (p *Provider) deleteFirewallRuleGroup(params map[string]any) (*plugin.Respo // ==================== FirewallRule ==================== func (p *Provider) createFirewallRule(params map[string]any) (*plugin.Response, error) { - groupID := str(params, "FirewallRuleGroupId") - domainListID := str(params, "FirewallDomainListId") - name := str(params, "Name") + groupID := shared.StrParam(params, "FirewallRuleGroupId") + domainListID := shared.StrParam(params, "FirewallDomainListId") + name := shared.StrParam(params, "Name") priority := intParam(params, "Priority") - action := str(params, "Action") + action := shared.StrParam(params, "Action") if action == "" { action = "BLOCK" } - blockResponse := str(params, "BlockResponse") + blockResponse := shared.StrParam(params, "BlockResponse") if blockResponse == "" { blockResponse = "NODATA" } @@ -903,7 +894,7 @@ func (p *Provider) createFirewallRule(params map[string]any) (*plugin.Response, } func (p *Provider) listFirewallRules(params map[string]any) (*plugin.Response, error) { - groupID := str(params, "FirewallRuleGroupId") + groupID := shared.StrParam(params, "FirewallRuleGroupId") rules, err := p.store.ListFirewallRules(groupID) if err != nil { return nil, err @@ -916,12 +907,12 @@ func (p *Provider) listFirewallRules(params map[string]any) (*plugin.Response, e } func (p *Provider) updateFirewallRule(params map[string]any) (*plugin.Response, error) { - groupID := str(params, "FirewallRuleGroupId") - domainListID := str(params, "FirewallDomainListId") - name := str(params, "Name") + groupID := shared.StrParam(params, "FirewallRuleGroupId") + domainListID := shared.StrParam(params, "FirewallDomainListId") + name := shared.StrParam(params, "Name") priority := intParam(params, "Priority") - action := str(params, "Action") - blockResponse := str(params, "BlockResponse") + action := shared.StrParam(params, "Action") + blockResponse := shared.StrParam(params, "BlockResponse") rule, err := p.store.UpdateFirewallRule(groupID, domainListID, name, priority, action, blockResponse) if err != nil { return shared.JSONError("ResourceNotFoundException", "firewall rule not found", http.StatusBadRequest), nil @@ -930,8 +921,8 @@ func (p *Provider) updateFirewallRule(params map[string]any) (*plugin.Response, } func (p *Provider) deleteFirewallRule(params map[string]any) (*plugin.Response, error) { - groupID := str(params, "FirewallRuleGroupId") - domainListID := str(params, "FirewallDomainListId") + groupID := shared.StrParam(params, "FirewallRuleGroupId") + domainListID := shared.StrParam(params, "FirewallDomainListId") // get before delete for response rules, _ := p.store.ListFirewallRules(groupID) var found *fwRuleRow @@ -953,7 +944,7 @@ func (p *Provider) deleteFirewallRule(params map[string]any) (*plugin.Response, // ==================== FirewallDomainList ==================== func (p *Provider) createFirewallDomainList(params map[string]any) (*plugin.Response, error) { - name := str(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return shared.JSONError("ValidationException", "Name is required", http.StatusBadRequest), nil } @@ -970,7 +961,7 @@ func (p *Provider) createFirewallDomainList(params map[string]any) (*plugin.Resp } func (p *Provider) getFirewallDomainList(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallDomainListId") + id := shared.StrParam(params, "FirewallDomainListId") dl, err := p.store.GetFirewallDomainList(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "firewall domain list not found", http.StatusBadRequest), nil @@ -995,7 +986,7 @@ func (p *Provider) listFirewallDomainLists(_ map[string]any) (*plugin.Response, } func (p *Provider) deleteFirewallDomainList(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallDomainListId") + id := shared.StrParam(params, "FirewallDomainListId") dl, err := p.store.GetFirewallDomainList(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "firewall domain list not found", http.StatusBadRequest), nil @@ -1008,7 +999,7 @@ func (p *Provider) deleteFirewallDomainList(params map[string]any) (*plugin.Resp } func (p *Provider) importFirewallDomains(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallDomainListId") + id := shared.StrParam(params, "FirewallDomainListId") dl, err := p.store.GetFirewallDomainList(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "firewall domain list not found", http.StatusBadRequest), nil @@ -1022,7 +1013,7 @@ func (p *Provider) importFirewallDomains(params map[string]any) (*plugin.Respons } func (p *Provider) listFirewallDomains(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallDomainListId") + id := shared.StrParam(params, "FirewallDomainListId") domains, err := p.store.ListFirewallDomains(id) if err != nil { return nil, err @@ -1034,8 +1025,8 @@ func (p *Provider) listFirewallDomains(params map[string]any) (*plugin.Response, } func (p *Provider) updateFirewallDomains(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallDomainListId") - op := str(params, "Operation") + id := shared.StrParam(params, "FirewallDomainListId") + op := shared.StrParam(params, "Operation") if op == "" { op = "ADD" } @@ -1065,9 +1056,9 @@ func (p *Provider) updateFirewallDomains(params map[string]any) (*plugin.Respons // ==================== FirewallRuleGroupAssociation ==================== func (p *Provider) associateFirewallRuleGroup(params map[string]any) (*plugin.Response, error) { - groupID := str(params, "FirewallRuleGroupId") - vpcID := str(params, "VpcId") - name := str(params, "Name") + groupID := shared.StrParam(params, "FirewallRuleGroupId") + vpcID := shared.StrParam(params, "VpcId") + name := shared.StrParam(params, "Name") priority := intParam(params, "Priority") id := shared.GenerateID("rslvr-frgassoc-", 28) assoc, err := p.store.AssociateFirewallRuleGroup(id, groupID, vpcID, name, priority) @@ -1078,7 +1069,7 @@ func (p *Provider) associateFirewallRuleGroup(params map[string]any) (*plugin.Re } func (p *Provider) getFirewallRuleGroupAssociation(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallRuleGroupAssociationId") + id := shared.StrParam(params, "FirewallRuleGroupAssociationId") assoc, err := p.store.GetFirewallRuleGroupAssociation(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "firewall rule group association not found", http.StatusBadRequest), nil @@ -1087,7 +1078,7 @@ func (p *Provider) getFirewallRuleGroupAssociation(params map[string]any) (*plug } func (p *Provider) listFirewallRuleGroupAssociations(params map[string]any) (*plugin.Response, error) { - vpcID := str(params, "VpcId") + vpcID := shared.StrParam(params, "VpcId") assocs, err := p.store.ListFirewallRuleGroupAssociations(vpcID) if err != nil { return nil, err @@ -1100,7 +1091,7 @@ func (p *Provider) listFirewallRuleGroupAssociations(params map[string]any) (*pl } func (p *Provider) disassociateFirewallRuleGroup(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallRuleGroupAssociationId") + id := shared.StrParam(params, "FirewallRuleGroupAssociationId") assoc, err := p.store.DisassociateFirewallRuleGroup(id) if err != nil { return shared.JSONError("ResourceNotFoundException", "firewall rule group association not found", http.StatusBadRequest), nil @@ -1109,8 +1100,8 @@ func (p *Provider) disassociateFirewallRuleGroup(params map[string]any) (*plugin } func (p *Provider) updateFirewallRuleGroupAssociation(params map[string]any) (*plugin.Response, error) { - id := str(params, "FirewallRuleGroupAssociationId") - name := str(params, "Name") + id := shared.StrParam(params, "FirewallRuleGroupAssociationId") + name := shared.StrParam(params, "Name") priority := intParam(params, "Priority") assoc, err := p.store.UpdateFirewallRuleGroupAssociation(id, name, priority) if err != nil { @@ -1130,7 +1121,7 @@ func (p *Provider) putFirewallRuleGroupPolicy(_ map[string]any) (*plugin.Respons // ==================== FirewallConfig (per-VPC, stub) ==================== func (p *Provider) getFirewallConfig(params map[string]any) (*plugin.Response, error) { - resourceID := str(params, "ResourceId") + resourceID := shared.StrParam(params, "ResourceId") return shared.JSONResponse(http.StatusOK, map[string]any{ "FirewallConfig": map[string]any{ "Id": shared.GenerateID("rslvr-fc-", 24), @@ -1142,8 +1133,8 @@ func (p *Provider) getFirewallConfig(params map[string]any) (*plugin.Response, e } func (p *Provider) updateFirewallConfig(params map[string]any) (*plugin.Response, error) { - resourceID := str(params, "ResourceId") - failOpen := str(params, "FirewallFailOpen") + resourceID := shared.StrParam(params, "ResourceId") + failOpen := shared.StrParam(params, "FirewallFailOpen") return shared.JSONResponse(http.StatusOK, map[string]any{ "FirewallConfig": map[string]any{ "Id": shared.GenerateID("rslvr-fc-", 24), @@ -1161,7 +1152,7 @@ func (p *Provider) listFirewallConfigs(_ map[string]any) (*plugin.Response, erro // ==================== ResolverConfig (stub) ==================== func (p *Provider) getResolverConfig(params map[string]any) (*plugin.Response, error) { - resourceID := str(params, "ResourceId") + resourceID := shared.StrParam(params, "ResourceId") return shared.JSONResponse(http.StatusOK, map[string]any{ "ResolverConfig": map[string]any{ "Id": shared.GenerateID("rslvr-rc-", 24), @@ -1173,8 +1164,8 @@ func (p *Provider) getResolverConfig(params map[string]any) (*plugin.Response, e } func (p *Provider) updateResolverConfig(params map[string]any) (*plugin.Response, error) { - resourceID := str(params, "ResourceId") - flag := str(params, "AutodefinedReverseFlag") + resourceID := shared.StrParam(params, "ResourceId") + flag := shared.StrParam(params, "AutodefinedReverseFlag") return shared.JSONResponse(http.StatusOK, map[string]any{ "ResolverConfig": map[string]any{ "Id": shared.GenerateID("rslvr-rc-", 24), @@ -1192,7 +1183,7 @@ func (p *Provider) listResolverConfigs(_ map[string]any) (*plugin.Response, erro // ==================== ResolverDnssecConfig (stub) ==================== func (p *Provider) getResolverDnssecConfig(params map[string]any) (*plugin.Response, error) { - resourceID := str(params, "ResourceId") + resourceID := shared.StrParam(params, "ResourceId") return shared.JSONResponse(http.StatusOK, map[string]any{ "ResolverDNSSECConfig": map[string]any{ "Id": shared.GenerateID("rslvr-dnssec-", 24), @@ -1204,8 +1195,8 @@ func (p *Provider) getResolverDnssecConfig(params map[string]any) (*plugin.Respo } func (p *Provider) updateResolverDnssecConfig(params map[string]any) (*plugin.Response, error) { - resourceID := str(params, "ResourceId") - validation := str(params, "Validation") + resourceID := shared.StrParam(params, "ResourceId") + validation := shared.StrParam(params, "Validation") return shared.JSONResponse(http.StatusOK, map[string]any{ "ResolverDNSSECConfig": map[string]any{ "Id": shared.GenerateID("rslvr-dnssec-", 24), @@ -1223,7 +1214,7 @@ func (p *Provider) listResolverDnssecConfigs(_ map[string]any) (*plugin.Response // ==================== Tags ==================== func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) { - arn := str(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") rawTags, _ := params["Tags"].([]any) if err := p.store.tags.AddTags(arn, parseTags(rawTags)); err != nil { return nil, err @@ -1232,7 +1223,7 @@ func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) } func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error) { - arn := str(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") rawKeys, _ := params["TagKeys"].([]any) keys := make([]string, 0, len(rawKeys)) for _, k := range rawKeys { @@ -1247,7 +1238,7 @@ func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error } func (p *Provider) listTagsForResource(params map[string]any) (*plugin.Response, error) { - arn := str(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") tags, err := p.store.tags.ListTags(arn) if err != nil { return nil, err diff --git a/internal/services/s3/provider.go b/internal/services/s3/provider.go index c8e90565..f14818c6 100644 --- a/internal/services/s3/provider.go +++ b/internal/services/s3/provider.go @@ -249,11 +249,6 @@ func (p *S3Provider) ListResources(ctx context.Context) ([]plugin.Resource, erro return resources, nil } -// GetMetrics returns empty metrics. -func (p *S3Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- XML response structs --- type listAllMyBucketsResult struct { diff --git a/internal/services/s3tables/provider.go b/internal/services/s3tables/provider.go index 55300aa0..cc5cc886 100644 --- a/internal/services/s3tables/provider.go +++ b/internal/services/s3tables/provider.go @@ -149,10 +149,6 @@ func (p *S3TablesProvider) ListResources(_ context.Context) ([]plugin.Resource, return res, nil } -func (p *S3TablesProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Bucket operations --- func (p *S3TablesProvider) createTableBucket(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/sagemaker/provider.go b/internal/services/sagemaker/provider.go index bd9d1156..d44fca5d 100644 --- a/internal/services/sagemaker/provider.go +++ b/internal/services/sagemaker/provider.go @@ -240,22 +240,18 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- NotebookInstance handlers ---- func (p *Provider) createNotebookInstance(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "NotebookInstanceName") + name := shared.StrParam(params, "NotebookInstanceName") if name == "" { return shared.JSONError("ValidationException", "NotebookInstanceName is required", http.StatusBadRequest), nil } - instanceType := strParam(params, "InstanceType") + instanceType := shared.StrParam(params, "InstanceType") if instanceType == "" { instanceType = "ml.t3.medium" } - roleARN := strParam(params, "RoleArn") + roleARN := shared.StrParam(params, "RoleArn") arn := shared.BuildARN("sagemaker", "notebook-instance", name) nb, err := p.store.CreateNotebookInstance(name, arn, "Pending", instanceType, roleARN, "") if err != nil { @@ -268,7 +264,7 @@ func (p *Provider) createNotebookInstance(params map[string]any) (*plugin.Respon } func (p *Provider) describeNotebookInstance(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "NotebookInstanceName") + name := shared.StrParam(params, "NotebookInstanceName") nb, err := p.store.GetNotebookInstance(name) if err != nil { return shared.JSONError("ResourceNotFound", "notebook instance not found: "+name, http.StatusBadRequest), nil @@ -303,7 +299,7 @@ func (p *Provider) listNotebookInstances(_ map[string]any) (*plugin.Response, er } func (p *Provider) deleteNotebookInstance(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "NotebookInstanceName") + name := shared.StrParam(params, "NotebookInstanceName") if err := p.store.DeleteNotebookInstance(name); err != nil { return shared.JSONError("ResourceNotFound", "notebook instance not found: "+name, http.StatusBadRequest), nil } @@ -311,7 +307,7 @@ func (p *Provider) deleteNotebookInstance(params map[string]any) (*plugin.Respon } func (p *Provider) startNotebookInstance(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "NotebookInstanceName") + name := shared.StrParam(params, "NotebookInstanceName") if err := p.store.UpdateNotebookInstanceStatus(name, "InService"); err != nil { return shared.JSONError("ResourceNotFound", "notebook instance not found: "+name, http.StatusBadRequest), nil } @@ -319,7 +315,7 @@ func (p *Provider) startNotebookInstance(params map[string]any) (*plugin.Respons } func (p *Provider) stopNotebookInstance(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "NotebookInstanceName") + name := shared.StrParam(params, "NotebookInstanceName") if err := p.store.UpdateNotebookInstanceStatus(name, "Stopped"); err != nil { return shared.JSONError("ResourceNotFound", "notebook instance not found: "+name, http.StatusBadRequest), nil } @@ -327,8 +323,8 @@ func (p *Provider) stopNotebookInstance(params map[string]any) (*plugin.Response } func (p *Provider) updateNotebookInstance(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "NotebookInstanceName") - instanceType := strParam(params, "InstanceType") + name := shared.StrParam(params, "NotebookInstanceName") + instanceType := shared.StrParam(params, "InstanceType") if instanceType == "" { nb, err := p.store.GetNotebookInstance(name) if err != nil { @@ -336,7 +332,7 @@ func (p *Provider) updateNotebookInstance(params map[string]any) (*plugin.Respon } instanceType = nb.InstanceType } - roleARN := strParam(params, "RoleArn") + roleARN := shared.StrParam(params, "RoleArn") if roleARN == "" { nb, err := p.store.GetNotebookInstance(name) if err != nil { @@ -353,11 +349,11 @@ func (p *Provider) updateNotebookInstance(params map[string]any) (*plugin.Respon // ---- Model handlers ---- func (p *Provider) createModel(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ModelName") + name := shared.StrParam(params, "ModelName") if name == "" { return shared.JSONError("ValidationException", "ModelName is required", http.StatusBadRequest), nil } - executionRole := strParam(params, "ExecutionRoleArn") + executionRole := shared.StrParam(params, "ExecutionRoleArn") primaryContainer := marshalParam(params, "PrimaryContainer") arn := shared.BuildARN("sagemaker", "model", name) m, err := p.store.CreateModel(name, arn, executionRole, primaryContainer) @@ -371,7 +367,7 @@ func (p *Provider) createModel(params map[string]any) (*plugin.Response, error) } func (p *Provider) describeModel(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ModelName") + name := shared.StrParam(params, "ModelName") m, err := p.store.GetModel(name) if err != nil { return shared.JSONError("ResourceNotFound", "model not found: "+name, http.StatusBadRequest), nil @@ -404,7 +400,7 @@ func (p *Provider) listModels(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) deleteModel(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ModelName") + name := shared.StrParam(params, "ModelName") if err := p.store.DeleteModel(name); err != nil { return shared.JSONError("ResourceNotFound", "model not found: "+name, http.StatusBadRequest), nil } @@ -414,7 +410,7 @@ func (p *Provider) deleteModel(params map[string]any) (*plugin.Response, error) // ---- EndpointConfig handlers ---- func (p *Provider) createEndpointConfig(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "EndpointConfigName") + name := shared.StrParam(params, "EndpointConfigName") if name == "" { return shared.JSONError("ValidationException", "EndpointConfigName is required", http.StatusBadRequest), nil } @@ -431,7 +427,7 @@ func (p *Provider) createEndpointConfig(params map[string]any) (*plugin.Response } func (p *Provider) describeEndpointConfig(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "EndpointConfigName") + name := shared.StrParam(params, "EndpointConfigName") ec, err := p.store.GetEndpointConfig(name) if err != nil { return shared.JSONError("ResourceNotFound", "endpoint config not found: "+name, http.StatusBadRequest), nil @@ -463,7 +459,7 @@ func (p *Provider) listEndpointConfigs(_ map[string]any) (*plugin.Response, erro } func (p *Provider) deleteEndpointConfig(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "EndpointConfigName") + name := shared.StrParam(params, "EndpointConfigName") if err := p.store.DeleteEndpointConfig(name); err != nil { return shared.JSONError("ResourceNotFound", "endpoint config not found: "+name, http.StatusBadRequest), nil } @@ -473,11 +469,11 @@ func (p *Provider) deleteEndpointConfig(params map[string]any) (*plugin.Response // ---- Endpoint handlers ---- func (p *Provider) createEndpoint(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "EndpointName") + name := shared.StrParam(params, "EndpointName") if name == "" { return shared.JSONError("ValidationException", "EndpointName is required", http.StatusBadRequest), nil } - configName := strParam(params, "EndpointConfigName") + configName := shared.StrParam(params, "EndpointConfigName") arn := shared.BuildARN("sagemaker", "endpoint", name) e, err := p.store.CreateEndpoint(name, arn, configName, "Creating") if err != nil { @@ -492,7 +488,7 @@ func (p *Provider) createEndpoint(params map[string]any) (*plugin.Response, erro } func (p *Provider) describeEndpoint(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "EndpointName") + name := shared.StrParam(params, "EndpointName") e, err := p.store.GetEndpoint(name) if err != nil { return shared.JSONError("ResourceNotFound", "endpoint not found: "+name, http.StatusBadRequest), nil @@ -525,8 +521,8 @@ func (p *Provider) listEndpoints(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) updateEndpoint(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "EndpointName") - configName := strParam(params, "EndpointConfigName") + name := shared.StrParam(params, "EndpointName") + configName := shared.StrParam(params, "EndpointConfigName") e, err := p.store.GetEndpoint(name) if err != nil { return shared.JSONError("ResourceNotFound", "endpoint not found: "+name, http.StatusBadRequest), nil @@ -538,7 +534,7 @@ func (p *Provider) updateEndpoint(params map[string]any) (*plugin.Response, erro } func (p *Provider) deleteEndpoint(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "EndpointName") + name := shared.StrParam(params, "EndpointName") if err := p.store.DeleteEndpoint(name); err != nil { return shared.JSONError("ResourceNotFound", "endpoint not found: "+name, http.StatusBadRequest), nil } @@ -548,11 +544,11 @@ func (p *Provider) deleteEndpoint(params map[string]any) (*plugin.Response, erro // ---- TrainingJob handlers ---- func (p *Provider) createTrainingJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TrainingJobName") + name := shared.StrParam(params, "TrainingJobName") if name == "" { return shared.JSONError("ValidationException", "TrainingJobName is required", http.StatusBadRequest), nil } - roleARN := strParam(params, "RoleArn") + roleARN := shared.StrParam(params, "RoleArn") algorithm := marshalParam(params, "AlgorithmSpecification") inputConfig := marshalParamArray(params, "InputDataConfig") outputConfig := marshalParam(params, "OutputDataConfig") @@ -569,7 +565,7 @@ func (p *Provider) createTrainingJob(params map[string]any) (*plugin.Response, e } func (p *Provider) describeTrainingJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TrainingJobName") + name := shared.StrParam(params, "TrainingJobName") tj, err := p.store.GetTrainingJob(name) if err != nil { return shared.JSONError("ResourceNotFound", "training job not found: "+name, http.StatusBadRequest), nil @@ -610,7 +606,7 @@ func (p *Provider) listTrainingJobs(_ map[string]any) (*plugin.Response, error) } func (p *Provider) stopTrainingJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TrainingJobName") + name := shared.StrParam(params, "TrainingJobName") if err := p.store.UpdateTrainingJobStatus(name, "Stopped"); err != nil { return shared.JSONError("ResourceNotFound", "training job not found: "+name, http.StatusBadRequest), nil } @@ -620,11 +616,11 @@ func (p *Provider) stopTrainingJob(params map[string]any) (*plugin.Response, err // ---- ProcessingJob handlers ---- func (p *Provider) createProcessingJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ProcessingJobName") + name := shared.StrParam(params, "ProcessingJobName") if name == "" { return shared.JSONError("ValidationException", "ProcessingJobName is required", http.StatusBadRequest), nil } - roleARN := strParam(params, "RoleArn") + roleARN := shared.StrParam(params, "RoleArn") appSpec := marshalParam(params, "AppSpecification") inputs := marshalParamArray(params, "ProcessingInputs") outputs := marshalParamArray(params, "ProcessingOutputConfig") @@ -641,7 +637,7 @@ func (p *Provider) createProcessingJob(params map[string]any) (*plugin.Response, } func (p *Provider) describeProcessingJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ProcessingJobName") + name := shared.StrParam(params, "ProcessingJobName") pj, err := p.store.GetProcessingJob(name) if err != nil { return shared.JSONError("ResourceNotFound", "processing job not found: "+name, http.StatusBadRequest), nil @@ -682,7 +678,7 @@ func (p *Provider) listProcessingJobs(_ map[string]any) (*plugin.Response, error } func (p *Provider) stopProcessingJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ProcessingJobName") + name := shared.StrParam(params, "ProcessingJobName") if err := p.store.UpdateProcessingJobStatus(name, "Stopped"); err != nil { return shared.JSONError("ResourceNotFound", "processing job not found: "+name, http.StatusBadRequest), nil } @@ -692,11 +688,11 @@ func (p *Provider) stopProcessingJob(params map[string]any) (*plugin.Response, e // ---- TransformJob handlers ---- func (p *Provider) createTransformJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TransformJobName") + name := shared.StrParam(params, "TransformJobName") if name == "" { return shared.JSONError("ValidationException", "TransformJobName is required", http.StatusBadRequest), nil } - modelName := strParam(params, "ModelName") + modelName := shared.StrParam(params, "ModelName") input := marshalParam(params, "TransformInput") output := marshalParam(params, "TransformOutput") resources := marshalParam(params, "TransformResources") @@ -712,7 +708,7 @@ func (p *Provider) createTransformJob(params map[string]any) (*plugin.Response, } func (p *Provider) describeTransformJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TransformJobName") + name := shared.StrParam(params, "TransformJobName") tj, err := p.store.GetTransformJob(name) if err != nil { return shared.JSONError("ResourceNotFound", "transform job not found: "+name, http.StatusBadRequest), nil @@ -751,7 +747,7 @@ func (p *Provider) listTransformJobs(_ map[string]any) (*plugin.Response, error) } func (p *Provider) stopTransformJob(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TransformJobName") + name := shared.StrParam(params, "TransformJobName") if err := p.store.UpdateTransformJobStatus(name, "Stopped"); err != nil { return shared.JSONError("ResourceNotFound", "transform job not found: "+name, http.StatusBadRequest), nil } @@ -761,13 +757,13 @@ func (p *Provider) stopTransformJob(params map[string]any) (*plugin.Response, er // ---- Pipeline handlers ---- func (p *Provider) createPipeline(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "PipelineName") + name := shared.StrParam(params, "PipelineName") if name == "" { return shared.JSONError("ValidationException", "PipelineName is required", http.StatusBadRequest), nil } - roleARN := strParam(params, "RoleArn") - definition := strParam(params, "PipelineDefinition") - description := strParam(params, "PipelineDescription") + roleARN := shared.StrParam(params, "RoleArn") + definition := shared.StrParam(params, "PipelineDefinition") + description := shared.StrParam(params, "PipelineDescription") arn := shared.BuildARN("sagemaker", "pipeline", name) pl, err := p.store.CreatePipeline(name, arn, roleARN, definition, description) if err != nil { @@ -780,7 +776,7 @@ func (p *Provider) createPipeline(params map[string]any) (*plugin.Response, erro } func (p *Provider) describePipeline(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "PipelineName") + name := shared.StrParam(params, "PipelineName") pl, err := p.store.GetPipeline(name) if err != nil { return shared.JSONError("ResourceNotFound", "pipeline not found: "+name, http.StatusBadRequest), nil @@ -816,20 +812,20 @@ func (p *Provider) listPipelines(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) updatePipeline(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "PipelineName") + name := shared.StrParam(params, "PipelineName") pl, err := p.store.GetPipeline(name) if err != nil { return shared.JSONError("ResourceNotFound", "pipeline not found: "+name, http.StatusBadRequest), nil } - roleARN := strParam(params, "RoleArn") + roleARN := shared.StrParam(params, "RoleArn") if roleARN == "" { roleARN = pl.RoleARN } - definition := strParam(params, "PipelineDefinition") + definition := shared.StrParam(params, "PipelineDefinition") if definition == "" { definition = pl.Definition } - description := strParam(params, "PipelineDescription") + description := shared.StrParam(params, "PipelineDescription") if description == "" { description = pl.Description } @@ -840,7 +836,7 @@ func (p *Provider) updatePipeline(params map[string]any) (*plugin.Response, erro } func (p *Provider) deletePipeline(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "PipelineName") + name := shared.StrParam(params, "PipelineName") pl, err := p.store.GetPipeline(name) if err != nil { return shared.JSONError("ResourceNotFound", "pipeline not found: "+name, http.StatusBadRequest), nil @@ -852,7 +848,7 @@ func (p *Provider) deletePipeline(params map[string]any) (*plugin.Response, erro } func (p *Provider) startPipelineExecution(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "PipelineName") + name := shared.StrParam(params, "PipelineName") if _, err := p.store.GetPipeline(name); err != nil { return shared.JSONError("ResourceNotFound", "pipeline not found: "+name, http.StatusBadRequest), nil } @@ -865,7 +861,7 @@ func (p *Provider) startPipelineExecution(params map[string]any) (*plugin.Respon } func (p *Provider) describePipelineExecution(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "PipelineExecutionArn") + arn := shared.StrParam(params, "PipelineExecutionArn") pe, err := p.store.GetPipelineExecution(arn) if err != nil { return shared.JSONError("ResourceNotFound", "pipeline execution not found: "+arn, http.StatusBadRequest), nil @@ -879,7 +875,7 @@ func (p *Provider) describePipelineExecution(params map[string]any) (*plugin.Res } func (p *Provider) listPipelineExecutions(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "PipelineName") + name := shared.StrParam(params, "PipelineName") executions, err := p.store.ListPipelineExecutions(name) if err != nil { return nil, err @@ -896,7 +892,7 @@ func (p *Provider) listPipelineExecutions(params map[string]any) (*plugin.Respon } func (p *Provider) stopPipelineExecution(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "PipelineExecutionArn") + arn := shared.StrParam(params, "PipelineExecutionArn") if err := p.store.UpdatePipelineExecutionStatus(arn, "Stopped"); err != nil { return shared.JSONError("ResourceNotFound", "pipeline execution not found: "+arn, http.StatusBadRequest), nil } @@ -906,11 +902,11 @@ func (p *Provider) stopPipelineExecution(params map[string]any) (*plugin.Respons // ---- Experiment handlers ---- func (p *Provider) createExperiment(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ExperimentName") + name := shared.StrParam(params, "ExperimentName") if name == "" { return shared.JSONError("ValidationException", "ExperimentName is required", http.StatusBadRequest), nil } - description := strParam(params, "Description") + description := shared.StrParam(params, "Description") arn := shared.BuildARN("sagemaker", "experiment", name) e, err := p.store.CreateExperiment(name, arn, description) if err != nil { @@ -923,7 +919,7 @@ func (p *Provider) createExperiment(params map[string]any) (*plugin.Response, er } func (p *Provider) describeExperiment(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ExperimentName") + name := shared.StrParam(params, "ExperimentName") e, err := p.store.GetExperiment(name) if err != nil { return shared.JSONError("ResourceNotFound", "experiment not found: "+name, http.StatusBadRequest), nil @@ -953,8 +949,8 @@ func (p *Provider) listExperiments(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) updateExperiment(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ExperimentName") - description := strParam(params, "Description") + name := shared.StrParam(params, "ExperimentName") + description := shared.StrParam(params, "Description") e, err := p.store.GetExperiment(name) if err != nil { return shared.JSONError("ResourceNotFound", "experiment not found: "+name, http.StatusBadRequest), nil @@ -966,7 +962,7 @@ func (p *Provider) updateExperiment(params map[string]any) (*plugin.Response, er } func (p *Provider) deleteExperiment(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "ExperimentName") + name := shared.StrParam(params, "ExperimentName") e, err := p.store.GetExperiment(name) if err != nil { return shared.JSONError("ResourceNotFound", "experiment not found: "+name, http.StatusBadRequest), nil @@ -980,11 +976,11 @@ func (p *Provider) deleteExperiment(params map[string]any) (*plugin.Response, er // ---- Trial handlers ---- func (p *Provider) createTrial(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TrialName") + name := shared.StrParam(params, "TrialName") if name == "" { return shared.JSONError("ValidationException", "TrialName is required", http.StatusBadRequest), nil } - experimentName := strParam(params, "ExperimentName") + experimentName := shared.StrParam(params, "ExperimentName") arn := shared.BuildARN("sagemaker", "experiment-trial", name) tr, err := p.store.CreateTrial(name, arn, experimentName) if err != nil { @@ -997,7 +993,7 @@ func (p *Provider) createTrial(params map[string]any) (*plugin.Response, error) } func (p *Provider) describeTrial(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TrialName") + name := shared.StrParam(params, "TrialName") tr, err := p.store.GetTrial(name) if err != nil { return shared.JSONError("ResourceNotFound", "trial not found: "+name, http.StatusBadRequest), nil @@ -1011,7 +1007,7 @@ func (p *Provider) describeTrial(params map[string]any) (*plugin.Response, error } func (p *Provider) listTrials(params map[string]any) (*plugin.Response, error) { - experimentName := strParam(params, "ExperimentName") + experimentName := shared.StrParam(params, "ExperimentName") trials, err := p.store.ListTrials(experimentName) if err != nil { return nil, err @@ -1029,8 +1025,8 @@ func (p *Provider) listTrials(params map[string]any) (*plugin.Response, error) { } func (p *Provider) updateTrial(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TrialName") - experimentName := strParam(params, "ExperimentName") + name := shared.StrParam(params, "TrialName") + experimentName := shared.StrParam(params, "ExperimentName") tr, err := p.store.GetTrial(name) if err != nil { return shared.JSONError("ResourceNotFound", "trial not found: "+name, http.StatusBadRequest), nil @@ -1045,7 +1041,7 @@ func (p *Provider) updateTrial(params map[string]any) (*plugin.Response, error) } func (p *Provider) deleteTrial(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "TrialName") + name := shared.StrParam(params, "TrialName") tr, err := p.store.GetTrial(name) if err != nil { return shared.JSONError("ResourceNotFound", "trial not found: "+name, http.StatusBadRequest), nil @@ -1059,15 +1055,15 @@ func (p *Provider) deleteTrial(params map[string]any) (*plugin.Response, error) // ---- Domain handlers ---- func (p *Provider) createDomain(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "DomainName") + name := shared.StrParam(params, "DomainName") if name == "" { return shared.JSONError("ValidationException", "DomainName is required", http.StatusBadRequest), nil } - authMode := strParam(params, "AuthMode") + authMode := shared.StrParam(params, "AuthMode") if authMode == "" { authMode = "IAM" } - vpcID := strParam(params, "VpcId") + vpcID := shared.StrParam(params, "VpcId") id := shared.GenerateID("d-", 16) arn := shared.BuildARN("sagemaker", "domain", id) d, err := p.store.CreateDomain(id, arn, name, "InService", authMode, vpcID) @@ -1081,7 +1077,7 @@ func (p *Provider) createDomain(params map[string]any) (*plugin.Response, error) } func (p *Provider) describeDomain(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "DomainId") + id := shared.StrParam(params, "DomainId") d, err := p.store.GetDomain(id) if err != nil { return shared.JSONError("ResourceNotFound", "domain not found: "+id, http.StatusBadRequest), nil @@ -1116,12 +1112,12 @@ func (p *Provider) listDomains(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) updateDomain(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "DomainId") + id := shared.StrParam(params, "DomainId") d, err := p.store.GetDomain(id) if err != nil { return shared.JSONError("ResourceNotFound", "domain not found: "+id, http.StatusBadRequest), nil } - vpcID := strParam(params, "VpcId") + vpcID := shared.StrParam(params, "VpcId") if vpcID == "" { vpcID = d.VpcID } @@ -1132,7 +1128,7 @@ func (p *Provider) updateDomain(params map[string]any) (*plugin.Response, error) } func (p *Provider) deleteDomain(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "DomainId") + id := shared.StrParam(params, "DomainId") if err := p.store.DeleteDomain(id); err != nil { return shared.JSONError("ResourceNotFound", "domain not found: "+id, http.StatusBadRequest), nil } @@ -1142,8 +1138,8 @@ func (p *Provider) deleteDomain(params map[string]any) (*plugin.Response, error) // ---- UserProfile handlers ---- func (p *Provider) createUserProfile(params map[string]any) (*plugin.Response, error) { - domainID := strParam(params, "DomainId") - name := strParam(params, "UserProfileName") + domainID := shared.StrParam(params, "DomainId") + name := shared.StrParam(params, "UserProfileName") if domainID == "" || name == "" { return shared.JSONError("ValidationException", "DomainId and UserProfileName are required", http.StatusBadRequest), nil } @@ -1159,8 +1155,8 @@ func (p *Provider) createUserProfile(params map[string]any) (*plugin.Response, e } func (p *Provider) describeUserProfile(params map[string]any) (*plugin.Response, error) { - domainID := strParam(params, "DomainId") - name := strParam(params, "UserProfileName") + domainID := shared.StrParam(params, "DomainId") + name := shared.StrParam(params, "UserProfileName") up, err := p.store.GetUserProfile(domainID, name) if err != nil { return shared.JSONError("ResourceNotFound", "user profile not found: "+name, http.StatusBadRequest), nil @@ -1175,7 +1171,7 @@ func (p *Provider) describeUserProfile(params map[string]any) (*plugin.Response, } func (p *Provider) listUserProfiles(params map[string]any) (*plugin.Response, error) { - domainID := strParam(params, "DomainIdEquals") + domainID := shared.StrParam(params, "DomainIdEquals") profiles, err := p.store.ListUserProfiles(domainID) if err != nil { return nil, err @@ -1194,8 +1190,8 @@ func (p *Provider) listUserProfiles(params map[string]any) (*plugin.Response, er } func (p *Provider) updateUserProfile(params map[string]any) (*plugin.Response, error) { - domainID := strParam(params, "DomainId") - name := strParam(params, "UserProfileName") + domainID := shared.StrParam(params, "DomainId") + name := shared.StrParam(params, "UserProfileName") up, err := p.store.GetUserProfile(domainID, name) if err != nil { return shared.JSONError("ResourceNotFound", "user profile not found: "+name, http.StatusBadRequest), nil @@ -1207,8 +1203,8 @@ func (p *Provider) updateUserProfile(params map[string]any) (*plugin.Response, e } func (p *Provider) deleteUserProfile(params map[string]any) (*plugin.Response, error) { - domainID := strParam(params, "DomainId") - name := strParam(params, "UserProfileName") + domainID := shared.StrParam(params, "DomainId") + name := shared.StrParam(params, "UserProfileName") if err := p.store.DeleteUserProfile(domainID, name); err != nil { return shared.JSONError("ResourceNotFound", "user profile not found: "+name, http.StatusBadRequest), nil } @@ -1218,7 +1214,7 @@ func (p *Provider) deleteUserProfile(params map[string]any) (*plugin.Response, e // ---- Tags handlers ---- func (p *Provider) addTags(params map[string]any) (*plugin.Response, error) { - resourceARN := strParam(params, "ResourceArn") + resourceARN := shared.StrParam(params, "ResourceArn") tagsRaw, _ := params["Tags"].([]any) tags := make(map[string]string, len(tagsRaw)) for _, t := range tagsRaw { @@ -1237,7 +1233,7 @@ func (p *Provider) addTags(params map[string]any) (*plugin.Response, error) { } func (p *Provider) deleteTags(params map[string]any) (*plugin.Response, error) { - resourceARN := strParam(params, "ResourceArn") + resourceARN := shared.StrParam(params, "ResourceArn") keys := stringsParam(params, "TagKeys") if err := p.store.RemoveTags(resourceARN, keys); err != nil { return nil, err @@ -1246,7 +1242,7 @@ func (p *Provider) deleteTags(params map[string]any) (*plugin.Response, error) { } func (p *Provider) listTags(params map[string]any) (*plugin.Response, error) { - resourceARN := strParam(params, "ResourceArn") + resourceARN := shared.StrParam(params, "ResourceArn") tags, err := p.store.ListTagsForResource(resourceARN) if err != nil { return nil, err @@ -1260,15 +1256,6 @@ func (p *Provider) listTags(params map[string]any) (*plugin.Response, error) { // ---- helpers ---- -func strParam(params map[string]any, key string) string { - if v, ok := params[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func marshalParam(params map[string]any, key string) string { if v, ok := params[key]; ok { b, _ := json.Marshal(v) diff --git a/internal/services/scheduler/provider.go b/internal/services/scheduler/provider.go index 183d567c..22bfcdb7 100644 --- a/internal/services/scheduler/provider.go +++ b/internal/services/scheduler/provider.go @@ -173,10 +173,6 @@ func (p *SchedulerProvider) ListResources(_ context.Context) ([]plugin.Resource, return res, nil } -func (p *SchedulerProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Schedule handlers --- func (p *SchedulerProvider) createSchedule(req *http.Request, params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/secretsmanager/provider.go b/internal/services/secretsmanager/provider.go index c763669a..c1fb9721 100644 --- a/internal/services/secretsmanager/provider.go +++ b/internal/services/secretsmanager/provider.go @@ -14,10 +14,12 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID -const defaultRegion = "us-east-1" +const defaultRegion = shared.DefaultRegion type Provider struct { store *SecretStore @@ -167,10 +169,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return resources, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) createSecret(params map[string]any) (*plugin.Response, error) { name, _ := params["Name"].(string) if name == "" { diff --git a/internal/services/serverlessrepo/provider.go b/internal/services/serverlessrepo/provider.go index d3a3da3b..9212e973 100644 --- a/internal/services/serverlessrepo/provider.go +++ b/internal/services/serverlessrepo/provider.go @@ -184,10 +184,6 @@ func (p *ServerlessRepoProvider) ListResources(_ context.Context) ([]plugin.Reso return res, nil } -func (p *ServerlessRepoProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Application handlers --- func (p *ServerlessRepoProvider) createApplication(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/servicediscovery/provider.go b/internal/services/servicediscovery/provider.go index 5e26fdca..4f55f6f3 100644 --- a/internal/services/servicediscovery/provider.go +++ b/internal/services/servicediscovery/provider.go @@ -142,10 +142,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // Namespace operations func (p *Provider) createHttpNamespace(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/ses/provider.go b/internal/services/ses/provider.go index 5535f6be..cb5efa70 100644 --- a/internal/services/ses/provider.go +++ b/internal/services/ses/provider.go @@ -212,10 +212,6 @@ func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func sesError(code, msg string, status int) *plugin.Response { diff --git a/internal/services/sesv2/provider.go b/internal/services/sesv2/provider.go index 4fc94387..436bc371 100644 --- a/internal/services/sesv2/provider.go +++ b/internal/services/sesv2/provider.go @@ -321,10 +321,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- EmailIdentity --- func (p *Provider) createEmailIdentity(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/sfn/provider.go b/internal/services/sfn/provider.go index e55941b4..1ff77f98 100644 --- a/internal/services/sfn/provider.go +++ b/internal/services/sfn/provider.go @@ -167,10 +167,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Helpers ---- func json10Resp(status int, v any) (*plugin.Response, error) { diff --git a/internal/services/shield/provider.go b/internal/services/shield/provider.go index 2d256259..123fe7c3 100644 --- a/internal/services/shield/provider.go +++ b/internal/services/shield/provider.go @@ -155,10 +155,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Protection --- func (p *Provider) createProtection(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/sns/provider.go b/internal/services/sns/provider.go index 4df65cde..10a5a833 100644 --- a/internal/services/sns/provider.go +++ b/internal/services/sns/provider.go @@ -14,10 +14,12 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID -const defaultRegion = "us-east-1" +const defaultRegion = shared.DefaultRegion type Provider struct { store *SNSStore @@ -114,10 +116,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return resources, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- operation handlers --- func (p *Provider) createTopic(req *http.Request) (*plugin.Response, error) { diff --git a/internal/services/sqs/provider.go b/internal/services/sqs/provider.go index b222d1d8..5913dd1e 100644 --- a/internal/services/sqs/provider.go +++ b/internal/services/sqs/provider.go @@ -14,6 +14,8 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" + + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID @@ -209,11 +211,6 @@ func (p *SQSProvider) ListResources(_ context.Context) ([]plugin.Resource, error return resources, nil } -// GetMetrics returns empty metrics. -func (p *SQSProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- XML response helpers --- type sqsErrorResponse struct { @@ -398,16 +395,6 @@ func jsonResp(status int, v any) (*plugin.Response, error) { }, nil } -// strParam extracts a string value from the JSON params map. -func strParam(params map[string]any, key string) string { - if v, ok := params[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - // intParam extracts an int value from the JSON params map. func intParam(params map[string]any, key string, defaultVal int) int { if v, ok := params[key]; ok { @@ -424,7 +411,7 @@ func intParam(params map[string]any, key string, defaultVal int) int { // --- JSON protocol operation implementations --- func (p *SQSProvider) createQueueJSON(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "QueueName") + name := shared.StrParam(params, "QueueName") if name == "" { return jsonError("MissingParameter", "QueueName is required", http.StatusBadRequest), nil } @@ -450,7 +437,7 @@ func (p *SQSProvider) createQueueJSON(params map[string]any) (*plugin.Response, } func (p *SQSProvider) listQueuesJSON(params map[string]any) (*plugin.Response, error) { - prefix := strParam(params, "QueueNamePrefix") + prefix := shared.StrParam(params, "QueueNamePrefix") queues := p.store.ListQueues(defaultAccountID, prefix) urls := make([]string, 0, len(queues)) @@ -461,7 +448,7 @@ func (p *SQSProvider) listQueuesJSON(params map[string]any) (*plugin.Response, e } func (p *SQSProvider) getQueueUrlJSON(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "QueueName") + name := shared.StrParam(params, "QueueName") if name == "" { return jsonError("MissingParameter", "QueueName is required", http.StatusBadRequest), nil } @@ -476,8 +463,8 @@ func (p *SQSProvider) getQueueUrlJSON(params map[string]any) (*plugin.Response, } func (p *SQSProvider) sendMessageJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") - body := strParam(params, "MessageBody") + queueURL := shared.StrParam(params, "QueueUrl") + body := shared.StrParam(params, "MessageBody") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -487,8 +474,8 @@ func (p *SQSProvider) sendMessageJSON(params map[string]any) (*plugin.Response, attrs := parseJSONMessageAttributes(params) fifoOpts := SendMessageFIFOOptions{ - MessageGroupID: strParam(params, "MessageGroupId"), - MessageDeduplicationID: strParam(params, "MessageDeduplicationId"), + MessageGroupID: shared.StrParam(params, "MessageGroupId"), + MessageDeduplicationID: shared.StrParam(params, "MessageDeduplicationId"), } name := queueNameFromURL(queueURL) @@ -504,7 +491,7 @@ func (p *SQSProvider) sendMessageJSON(params map[string]any) (*plugin.Response, } func (p *SQSProvider) receiveMessageJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -556,8 +543,8 @@ func (p *SQSProvider) receiveMessageJSON(params map[string]any) (*plugin.Respons } func (p *SQSProvider) deleteMessageJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") - receiptHandle := strParam(params, "ReceiptHandle") + queueURL := shared.StrParam(params, "QueueUrl") + receiptHandle := shared.StrParam(params, "ReceiptHandle") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -575,7 +562,7 @@ func (p *SQSProvider) deleteMessageJSON(params map[string]any) (*plugin.Response } func (p *SQSProvider) deleteQueueJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -590,7 +577,7 @@ func (p *SQSProvider) deleteQueueJSON(params map[string]any) (*plugin.Response, } func (p *SQSProvider) getQueueAttributesJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -619,7 +606,7 @@ func (p *SQSProvider) getQueueAttributesJSON(params map[string]any) (*plugin.Res } func (p *SQSProvider) setQueueAttributesJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -645,7 +632,7 @@ func (p *SQSProvider) setQueueAttributesJSON(params map[string]any) (*plugin.Res } func (p *SQSProvider) purgeQueueJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -660,7 +647,7 @@ func (p *SQSProvider) purgeQueueJSON(params map[string]any) (*plugin.Response, e } func (p *SQSProvider) listDeadLetterSourceQueuesJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -677,11 +664,11 @@ func (p *SQSProvider) listDeadLetterSourceQueuesJSON(params map[string]any) (*pl } func (p *SQSProvider) startMessageMoveTaskJSON(params map[string]any) (*plugin.Response, error) { - sourceArn := strParam(params, "SourceArn") + sourceArn := shared.StrParam(params, "SourceArn") if sourceArn == "" { return jsonError("MissingParameter", "SourceArn is required", http.StatusBadRequest), nil } - destArn := strParam(params, "DestinationArn") + destArn := shared.StrParam(params, "DestinationArn") maxRate := intParam(params, "MaxNumberOfMessagesPerSecond", 0) handle, err := p.store.StartMessageMoveTask(sourceArn, destArn, maxRate, defaultAccountID) @@ -697,7 +684,7 @@ func (p *SQSProvider) startMessageMoveTaskJSON(params map[string]any) (*plugin.R } func (p *SQSProvider) listMessageMoveTasksJSON(params map[string]any) (*plugin.Response, error) { - sourceArn := strParam(params, "SourceArn") + sourceArn := shared.StrParam(params, "SourceArn") if sourceArn == "" { return jsonError("MissingParameter", "SourceArn is required", http.StatusBadRequest), nil } @@ -724,7 +711,7 @@ func (p *SQSProvider) listMessageMoveTasksJSON(params map[string]any) (*plugin.R } func (p *SQSProvider) cancelMessageMoveTaskJSON(params map[string]any) (*plugin.Response, error) { - handle := strParam(params, "TaskHandle") + handle := shared.StrParam(params, "TaskHandle") if handle == "" { return jsonError("MissingParameter", "TaskHandle is required", http.StatusBadRequest), nil } @@ -1159,7 +1146,7 @@ func parseFormMessageAttributes(req *http.Request) map[string]MessageAttribute { // --- Batch operations --- func (p *SQSProvider) sendMessageBatchJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -1191,8 +1178,8 @@ func (p *SQSProvider) sendMessageBatchJSON(params map[string]any) (*plugin.Respo if !ok { continue } - id := strParam(entry, "Id") - body := strParam(entry, "MessageBody") + id := shared.StrParam(entry, "Id") + body := shared.StrParam(entry, "MessageBody") if body == "" { failed = append(failed, failEntry{ Id: id, @@ -1204,8 +1191,8 @@ func (p *SQSProvider) sendMessageBatchJSON(params map[string]any) (*plugin.Respo } attrs := parseJSONMessageAttributes(entry) batchFIFOOpts := SendMessageFIFOOptions{ - MessageGroupID: strParam(entry, "MessageGroupId"), - MessageDeduplicationID: strParam(entry, "MessageDeduplicationId"), + MessageGroupID: shared.StrParam(entry, "MessageGroupId"), + MessageDeduplicationID: shared.StrParam(entry, "MessageDeduplicationId"), } msgID, err := p.store.SendMessageFull(name, defaultAccountID, body, attrs, batchFIFOOpts) if err != nil { @@ -1238,7 +1225,7 @@ func (p *SQSProvider) sendMessageBatchJSON(params map[string]any) (*plugin.Respo } func (p *SQSProvider) deleteMessageBatchJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -1268,8 +1255,8 @@ func (p *SQSProvider) deleteMessageBatchJSON(params map[string]any) (*plugin.Res if !ok { continue } - id := strParam(entry, "Id") - receiptHandle := strParam(entry, "ReceiptHandle") + id := shared.StrParam(entry, "Id") + receiptHandle := shared.StrParam(entry, "ReceiptHandle") err := p.store.DeleteMessage(name, defaultAccountID, receiptHandle) if err != nil { failed = append(failed, failEntry{ @@ -1297,7 +1284,7 @@ func (p *SQSProvider) deleteMessageBatchJSON(params map[string]any) (*plugin.Res } func (p *SQSProvider) changeMessageVisibilityBatchJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -1327,8 +1314,8 @@ func (p *SQSProvider) changeMessageVisibilityBatchJSON(params map[string]any) (* if !ok { continue } - id := strParam(entry, "Id") - receiptHandle := strParam(entry, "ReceiptHandle") + id := shared.StrParam(entry, "Id") + receiptHandle := shared.StrParam(entry, "ReceiptHandle") vt := intParam(entry, "VisibilityTimeout", 30) err := p.store.ChangeMessageVisibility(defaultAccountID, name, receiptHandle, vt) if err != nil { @@ -1357,8 +1344,8 @@ func (p *SQSProvider) changeMessageVisibilityBatchJSON(params map[string]any) (* } func (p *SQSProvider) changeMessageVisibilityJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") - receiptHandle := strParam(params, "ReceiptHandle") + queueURL := shared.StrParam(params, "QueueUrl") + receiptHandle := shared.StrParam(params, "ReceiptHandle") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -1379,7 +1366,7 @@ func (p *SQSProvider) changeMessageVisibilityJSON(params map[string]any) (*plugi // --- Tags --- func (p *SQSProvider) tagQueueJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -1405,7 +1392,7 @@ func (p *SQSProvider) tagQueueJSON(params map[string]any) (*plugin.Response, err } func (p *SQSProvider) untagQueueJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } @@ -1431,7 +1418,7 @@ func (p *SQSProvider) untagQueueJSON(params map[string]any) (*plugin.Response, e } func (p *SQSProvider) listQueueTagsJSON(params map[string]any) (*plugin.Response, error) { - queueURL := strParam(params, "QueueUrl") + queueURL := shared.StrParam(params, "QueueUrl") if queueURL == "" { return jsonError("MissingParameter", "QueueUrl is required", http.StatusBadRequest), nil } diff --git a/internal/services/ssm/provider.go b/internal/services/ssm/provider.go index c306f328..b1d9146a 100644 --- a/internal/services/ssm/provider.go +++ b/internal/services/ssm/provider.go @@ -129,10 +129,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return resources, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) putParameter(params map[string]any) (*plugin.Response, error) { name, _ := params["Name"].(string) value, _ := params["Value"].(string) diff --git a/internal/services/ssoadmin/provider.go b/internal/services/ssoadmin/provider.go index 419190e4..1d6ea562 100644 --- a/internal/services/ssoadmin/provider.go +++ b/internal/services/ssoadmin/provider.go @@ -240,10 +240,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Instance --- func (p *Provider) createInstance(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/support/provider.go b/internal/services/support/provider.go index 9f75d312..1c18a940 100644 --- a/internal/services/support/provider.go +++ b/internal/services/support/provider.go @@ -169,10 +169,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - func (p *Provider) createCase(params map[string]any) (*plugin.Response, error) { subject, _ := params["subject"].(string) if subject == "" { diff --git a/internal/services/swf/provider.go b/internal/services/swf/provider.go index 29107dfd..7a3c4e22 100644 --- a/internal/services/swf/provider.go +++ b/internal/services/swf/provider.go @@ -170,10 +170,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Helpers ---- func json10Resp(status int, v any) (*plugin.Response, error) { @@ -213,11 +209,6 @@ func parseTags(rawTags []any) map[string]string { return tags } -func strParam(params map[string]any, key string) string { - v, _ := params[key].(string) - return v -} - func typeInfoParam(params map[string]any, key string) (name, version string) { m, _ := params[key].(map[string]any) if m == nil { @@ -231,12 +222,12 @@ func typeInfoParam(params map[string]any, key string) (name, version string) { // ---- Domain handlers ---- func (p *Provider) registerDomain(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") + name := shared.StrParam(params, "name") if name == "" { return json10Err("ValidationException", "name is required", http.StatusBadRequest), nil } - description := strParam(params, "description") - retention := strParam(params, "workflowExecutionRetentionPeriodInDays") + description := shared.StrParam(params, "description") + retention := shared.StrParam(params, "workflowExecutionRetentionPeriodInDays") if retention == "" { retention = "30" } @@ -262,7 +253,7 @@ func (p *Provider) registerDomain(params map[string]any) (*plugin.Response, erro } func (p *Provider) describeDomain(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") + name := shared.StrParam(params, "name") if name == "" { return json10Err("ValidationException", "name is required", http.StatusBadRequest), nil } @@ -274,7 +265,7 @@ func (p *Provider) describeDomain(params map[string]any) (*plugin.Response, erro } func (p *Provider) listDomains(params map[string]any) (*plugin.Response, error) { - statusFilter := strParam(params, "registrationStatus") + statusFilter := shared.StrParam(params, "registrationStatus") domains, err := p.store.ListDomains(statusFilter) if err != nil { return nil, err @@ -291,7 +282,7 @@ func (p *Provider) listDomains(params map[string]any) (*plugin.Response, error) } func (p *Provider) deprecateDomain(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") + name := shared.StrParam(params, "name") if name == "" { return json10Err("ValidationException", "name is required", http.StatusBadRequest), nil } @@ -309,7 +300,7 @@ func (p *Provider) deprecateDomain(params map[string]any) (*plugin.Response, err } func (p *Provider) undeprecateDomain(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "name") + name := shared.StrParam(params, "name") if name == "" { return json10Err("ValidationException", "name is required", http.StatusBadRequest), nil } @@ -325,9 +316,9 @@ func (p *Provider) undeprecateDomain(params map[string]any) (*plugin.Response, e // ---- WorkflowType handlers ---- func (p *Provider) registerWorkflowType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") - name := strParam(params, "name") - version := strParam(params, "version") + domain := shared.StrParam(params, "domain") + name := shared.StrParam(params, "name") + version := shared.StrParam(params, "version") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain, name, version are required", http.StatusBadRequest), nil } @@ -337,7 +328,7 @@ func (p *Provider) registerWorkflowType(params map[string]any) (*plugin.Response Name: name, Version: version, Status: "REGISTERED", - Description: strParam(params, "description"), + Description: shared.StrParam(params, "description"), DefaultTimeout: "NONE", CreatedAt: now, } @@ -351,7 +342,7 @@ func (p *Provider) registerWorkflowType(params map[string]any) (*plugin.Response } func (p *Provider) describeWorkflowType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") name, version := typeInfoParam(params, "workflowType") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain and workflowType are required", http.StatusBadRequest), nil @@ -364,11 +355,11 @@ func (p *Provider) describeWorkflowType(params map[string]any) (*plugin.Response } func (p *Provider) listWorkflowTypes(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") if domain == "" { return json10Err("ValidationException", "domain is required", http.StatusBadRequest), nil } - statusFilter := strParam(params, "registrationStatus") + statusFilter := shared.StrParam(params, "registrationStatus") wts, err := p.store.ListWorkflowTypes(domain, statusFilter) if err != nil { return nil, err @@ -383,7 +374,7 @@ func (p *Provider) listWorkflowTypes(params map[string]any) (*plugin.Response, e } func (p *Provider) deprecateWorkflowType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") name, version := typeInfoParam(params, "workflowType") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain and workflowType are required", http.StatusBadRequest), nil @@ -402,7 +393,7 @@ func (p *Provider) deprecateWorkflowType(params map[string]any) (*plugin.Respons } func (p *Provider) undeprecateWorkflowType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") name, version := typeInfoParam(params, "workflowType") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain and workflowType are required", http.StatusBadRequest), nil @@ -417,7 +408,7 @@ func (p *Provider) undeprecateWorkflowType(params map[string]any) (*plugin.Respo } func (p *Provider) deleteWorkflowType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") name, version := typeInfoParam(params, "workflowType") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain and workflowType are required", http.StatusBadRequest), nil @@ -438,9 +429,9 @@ func (p *Provider) deleteWorkflowType(params map[string]any) (*plugin.Response, // ---- ActivityType handlers ---- func (p *Provider) registerActivityType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") - name := strParam(params, "name") - version := strParam(params, "version") + domain := shared.StrParam(params, "domain") + name := shared.StrParam(params, "name") + version := shared.StrParam(params, "version") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain, name, version are required", http.StatusBadRequest), nil } @@ -450,7 +441,7 @@ func (p *Provider) registerActivityType(params map[string]any) (*plugin.Response Name: name, Version: version, Status: "REGISTERED", - Description: strParam(params, "description"), + Description: shared.StrParam(params, "description"), DefaultTimeout: "NONE", CreatedAt: now, } @@ -464,7 +455,7 @@ func (p *Provider) registerActivityType(params map[string]any) (*plugin.Response } func (p *Provider) describeActivityType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") name, version := typeInfoParam(params, "activityType") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain and activityType are required", http.StatusBadRequest), nil @@ -477,11 +468,11 @@ func (p *Provider) describeActivityType(params map[string]any) (*plugin.Response } func (p *Provider) listActivityTypes(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") if domain == "" { return json10Err("ValidationException", "domain is required", http.StatusBadRequest), nil } - statusFilter := strParam(params, "registrationStatus") + statusFilter := shared.StrParam(params, "registrationStatus") ats, err := p.store.ListActivityTypes(domain, statusFilter) if err != nil { return nil, err @@ -496,7 +487,7 @@ func (p *Provider) listActivityTypes(params map[string]any) (*plugin.Response, e } func (p *Provider) deprecateActivityType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") name, version := typeInfoParam(params, "activityType") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain and activityType are required", http.StatusBadRequest), nil @@ -515,7 +506,7 @@ func (p *Provider) deprecateActivityType(params map[string]any) (*plugin.Respons } func (p *Provider) undeprecateActivityType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") name, version := typeInfoParam(params, "activityType") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain and activityType are required", http.StatusBadRequest), nil @@ -530,7 +521,7 @@ func (p *Provider) undeprecateActivityType(params map[string]any) (*plugin.Respo } func (p *Provider) deleteActivityType(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") name, version := typeInfoParam(params, "activityType") if domain == "" || name == "" || version == "" { return json10Err("ValidationException", "domain and activityType are required", http.StatusBadRequest), nil @@ -551,13 +542,13 @@ func (p *Provider) deleteActivityType(params map[string]any) (*plugin.Response, // ---- WorkflowExecution handlers ---- func (p *Provider) startWorkflowExecution(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") - workflowID := strParam(params, "workflowId") + domain := shared.StrParam(params, "domain") + workflowID := shared.StrParam(params, "workflowId") if domain == "" || workflowID == "" { return json10Err("ValidationException", "domain and workflowId are required", http.StatusBadRequest), nil } wfName, wfVersion := typeInfoParam(params, "workflowType") - input := strParam(params, "input") + input := shared.StrParam(params, "input") runID := shared.GenerateUUID() now := time.Now() @@ -590,7 +581,7 @@ func (p *Provider) startWorkflowExecution(params map[string]any) (*plugin.Respon } func (p *Provider) describeWorkflowExecution(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") execMap, _ := params["execution"].(map[string]any) if domain == "" || execMap == nil { return json10Err("ValidationException", "domain and execution are required", http.StatusBadRequest), nil @@ -608,7 +599,7 @@ func (p *Provider) describeWorkflowExecution(params map[string]any) (*plugin.Res } func (p *Provider) listOpenWorkflowExecutions(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") if domain == "" { return json10Err("ValidationException", "domain is required", http.StatusBadRequest), nil } @@ -626,7 +617,7 @@ func (p *Provider) listOpenWorkflowExecutions(params map[string]any) (*plugin.Re } func (p *Provider) listClosedWorkflowExecutions(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") if domain == "" { return json10Err("ValidationException", "domain is required", http.StatusBadRequest), nil } @@ -644,12 +635,12 @@ func (p *Provider) listClosedWorkflowExecutions(params map[string]any) (*plugin. } func (p *Provider) terminateWorkflowExecution(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") - workflowID := strParam(params, "workflowId") + domain := shared.StrParam(params, "domain") + workflowID := shared.StrParam(params, "workflowId") if domain == "" || workflowID == "" { return json10Err("ValidationException", "domain and workflowId are required", http.StatusBadRequest), nil } - runID := strParam(params, "runId") + runID := shared.StrParam(params, "runId") var we *WorkflowExecution var err error if runID != "" { @@ -667,12 +658,12 @@ func (p *Provider) terminateWorkflowExecution(params map[string]any) (*plugin.Re } func (p *Provider) requestCancelWorkflowExecution(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") - workflowID := strParam(params, "workflowId") + domain := shared.StrParam(params, "domain") + workflowID := shared.StrParam(params, "workflowId") if domain == "" || workflowID == "" { return json10Err("ValidationException", "domain and workflowId are required", http.StatusBadRequest), nil } - runID := strParam(params, "runId") + runID := shared.StrParam(params, "runId") var we *WorkflowExecution var err error if runID != "" { @@ -694,7 +685,7 @@ func (p *Provider) signalWorkflowExecution(_ map[string]any) (*plugin.Response, } func (p *Provider) getWorkflowExecutionHistory(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") execMap, _ := params["execution"].(map[string]any) if domain == "" || execMap == nil { return json10Err("ValidationException", "domain and execution are required", http.StatusBadRequest), nil @@ -725,7 +716,7 @@ func (p *Provider) getWorkflowExecutionHistory(params map[string]any) (*plugin.R // ---- Count handlers ---- func (p *Provider) countOpenWorkflowExecutions(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") if domain == "" { return json10Err("ValidationException", "domain is required", http.StatusBadRequest), nil } @@ -737,7 +728,7 @@ func (p *Provider) countOpenWorkflowExecutions(params map[string]any) (*plugin.R } func (p *Provider) countClosedWorkflowExecutions(params map[string]any) (*plugin.Response, error) { - domain := strParam(params, "domain") + domain := shared.StrParam(params, "domain") if domain == "" { return json10Err("ValidationException", "domain is required", http.StatusBadRequest), nil } @@ -796,7 +787,7 @@ func (p *Provider) recordActivityTaskHeartbeat(_ map[string]any) (*plugin.Respon // ---- Tag handlers ---- func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "resourceArn") + arn := shared.StrParam(params, "resourceArn") if arn == "" { return json10Err("ValidationException", "resourceArn is required", http.StatusBadRequest), nil } @@ -807,7 +798,7 @@ func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) } func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "resourceArn") + arn := shared.StrParam(params, "resourceArn") if arn == "" { return json10Err("ValidationException", "resourceArn is required", http.StatusBadRequest), nil } @@ -824,7 +815,7 @@ func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error } func (p *Provider) listTagsForResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "resourceArn") + arn := shared.StrParam(params, "resourceArn") if arn == "" { return json10Err("ValidationException", "resourceArn is required", http.StatusBadRequest), nil } diff --git a/internal/services/textract/provider.go b/internal/services/textract/provider.go index d2cdc88f..a15c6db3 100644 --- a/internal/services/textract/provider.go +++ b/internal/services/textract/provider.go @@ -185,10 +185,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Adapter operations --- func (p *Provider) createAdapter(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/timestreamwrite/provider.go b/internal/services/timestreamwrite/provider.go index a4c700b1..248d17c1 100644 --- a/internal/services/timestreamwrite/provider.go +++ b/internal/services/timestreamwrite/provider.go @@ -121,10 +121,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- Database handlers ---- func (p *Provider) createDatabase(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/transcribe/provider.go b/internal/services/transcribe/provider.go index 7f1e7d17..c115dacc 100644 --- a/internal/services/transcribe/provider.go +++ b/internal/services/transcribe/provider.go @@ -178,10 +178,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // ---- TranscriptionJob ---- func (p *Provider) startTranscriptionJob(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/transfer/provider.go b/internal/services/transfer/provider.go index 9fc40dd6..07801af7 100644 --- a/internal/services/transfer/provider.go +++ b/internal/services/transfer/provider.go @@ -166,10 +166,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // Server operations func (p *Provider) createServer(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/verifiedpermissions/provider.go b/internal/services/verifiedpermissions/provider.go index 18584f46..f54fd6e4 100644 --- a/internal/services/verifiedpermissions/provider.go +++ b/internal/services/verifiedpermissions/provider.go @@ -123,11 +123,6 @@ func (p *VerifiedPermissionsProvider) ListResources(ctx context.Context) ([]plug return []plugin.Resource{}, nil } -// GetMetrics returns empty metrics. -func (p *VerifiedPermissionsProvider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func jsonError(code, message string, status int) *plugin.Response { diff --git a/internal/services/waf/provider.go b/internal/services/waf/provider.go index afcbf406..2657495c 100644 --- a/internal/services/waf/provider.go +++ b/internal/services/waf/provider.go @@ -274,10 +274,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return out, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- helpers --- func wafError(code, msg string, status int) *plugin.Response { @@ -292,15 +288,6 @@ func wafARN(resourceType, id string) string { return fmt.Sprintf("arn:aws:waf::%s:%s/%s", accountID, resourceType, id) } -func strParam(params map[string]any, key string) string { - if v, ok := params[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - func marshalJSON(v any) string { b, _ := json.Marshal(v) return string(b) @@ -320,11 +307,11 @@ func (p *Provider) getChangeTokenStatus(_ map[string]any) (*plugin.Response, err // --- WebACL --- func (p *Provider) createWebACL(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } - changeToken := strParam(params, "ChangeToken") + changeToken := shared.StrParam(params, "ChangeToken") id := shared.GenerateUUID() arn := wafARN("webacl", id) @@ -363,7 +350,7 @@ func (p *Provider) createWebACL(params map[string]any) (*plugin.Response, error) } func (p *Provider) getWebACL(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "WebACLId") + id := shared.StrParam(params, "WebACLId") if id == "" { return wafError("WAFInvalidParameterException", "WebACLId is required", http.StatusBadRequest), nil } @@ -407,7 +394,7 @@ func (p *Provider) listWebACLs(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) updateWebACL(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "WebACLId") + id := shared.StrParam(params, "WebACLId") if id == "" { return wafError("WAFInvalidParameterException", "WebACLId is required", http.StatusBadRequest), nil } @@ -441,7 +428,7 @@ func (p *Provider) updateWebACL(params map[string]any) (*plugin.Response, error) } w.Rules = marshalJSON(rules) } - w.ChangeToken = strParam(params, "ChangeToken") + w.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateWebACL(w); err != nil { return nil, err } @@ -449,7 +436,7 @@ func (p *Provider) updateWebACL(params map[string]any) (*plugin.Response, error) } func (p *Provider) deleteWebACL(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "WebACLId") + id := shared.StrParam(params, "WebACLId") if id == "" { return wafError("WAFInvalidParameterException", "WebACLId is required", http.StatusBadRequest), nil } @@ -465,7 +452,7 @@ func (p *Provider) deleteWebACL(params map[string]any) (*plugin.Response, error) // --- IPSet --- func (p *Provider) createIPSet(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } @@ -476,7 +463,7 @@ func (p *Provider) createIPSet(params map[string]any) (*plugin.Response, error) Name: name, ARN: arn, Descriptors: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateIPSet(ip); err != nil { return nil, err @@ -492,7 +479,7 @@ func (p *Provider) createIPSet(params map[string]any) (*plugin.Response, error) } func (p *Provider) getIPSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "IPSetId") + id := shared.StrParam(params, "IPSetId") if id == "" { return wafError("WAFInvalidParameterException", "IPSetId is required", http.StatusBadRequest), nil } @@ -530,7 +517,7 @@ func (p *Provider) listIPSets(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) updateIPSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "IPSetId") + id := shared.StrParam(params, "IPSetId") if id == "" { return wafError("WAFInvalidParameterException", "IPSetId is required", http.StatusBadRequest), nil } @@ -559,7 +546,7 @@ func (p *Provider) updateIPSet(params map[string]any) (*plugin.Response, error) } ip.Descriptors = marshalJSON(descs) } - ip.ChangeToken = strParam(params, "ChangeToken") + ip.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateIPSet(ip); err != nil { return nil, err } @@ -567,7 +554,7 @@ func (p *Provider) updateIPSet(params map[string]any) (*plugin.Response, error) } func (p *Provider) deleteIPSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "IPSetId") + id := shared.StrParam(params, "IPSetId") if id == "" { return wafError("WAFInvalidParameterException", "IPSetId is required", http.StatusBadRequest), nil } @@ -583,11 +570,11 @@ func (p *Provider) deleteIPSet(params map[string]any) (*plugin.Response, error) // --- Rule --- func (p *Provider) createRule(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } - metricName := strParam(params, "MetricName") + metricName := shared.StrParam(params, "MetricName") if metricName == "" { metricName = name } @@ -599,7 +586,7 @@ func (p *Provider) createRule(params map[string]any) (*plugin.Response, error) { ARN: arn, MetricName: metricName, Predicates: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateRule(r); err != nil { return nil, err @@ -616,7 +603,7 @@ func (p *Provider) createRule(params map[string]any) (*plugin.Response, error) { } func (p *Provider) getRule(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleId") + id := shared.StrParam(params, "RuleId") if id == "" { return wafError("WAFInvalidParameterException", "RuleId is required", http.StatusBadRequest), nil } @@ -655,7 +642,7 @@ func (p *Provider) listRules(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) updateRule(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleId") + id := shared.StrParam(params, "RuleId") if id == "" { return wafError("WAFInvalidParameterException", "RuleId is required", http.StatusBadRequest), nil } @@ -684,7 +671,7 @@ func (p *Provider) updateRule(params map[string]any) (*plugin.Response, error) { } r.Predicates = marshalJSON(preds) } - r.ChangeToken = strParam(params, "ChangeToken") + r.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateRule(r); err != nil { return nil, err } @@ -692,7 +679,7 @@ func (p *Provider) updateRule(params map[string]any) (*plugin.Response, error) { } func (p *Provider) deleteRule(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleId") + id := shared.StrParam(params, "RuleId") if id == "" { return wafError("WAFInvalidParameterException", "RuleId is required", http.StatusBadRequest), nil } @@ -708,11 +695,11 @@ func (p *Provider) deleteRule(params map[string]any) (*plugin.Response, error) { // --- RuleGroup --- func (p *Provider) createRuleGroup(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } - metricName := strParam(params, "MetricName") + metricName := shared.StrParam(params, "MetricName") if metricName == "" { metricName = name } @@ -723,7 +710,7 @@ func (p *Provider) createRuleGroup(params map[string]any) (*plugin.Response, err Name: name, ARN: arn, MetricName: metricName, - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateRuleGroup(rg); err != nil { return nil, err @@ -739,7 +726,7 @@ func (p *Provider) createRuleGroup(params map[string]any) (*plugin.Response, err } func (p *Provider) getRuleGroup(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleGroupId") + id := shared.StrParam(params, "RuleGroupId") if id == "" { return wafError("WAFInvalidParameterException", "RuleGroupId is required", http.StatusBadRequest), nil } @@ -772,7 +759,7 @@ func (p *Provider) listRuleGroups(_ map[string]any) (*plugin.Response, error) { } func (p *Provider) updateRuleGroup(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleGroupId") + id := shared.StrParam(params, "RuleGroupId") if id == "" { return wafError("WAFInvalidParameterException", "RuleGroupId is required", http.StatusBadRequest), nil } @@ -786,7 +773,7 @@ func (p *Provider) updateRuleGroup(params map[string]any) (*plugin.Response, err } func (p *Provider) deleteRuleGroup(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleGroupId") + id := shared.StrParam(params, "RuleGroupId") if id == "" { return wafError("WAFInvalidParameterException", "RuleGroupId is required", http.StatusBadRequest), nil } @@ -802,11 +789,11 @@ func (p *Provider) deleteRuleGroup(params map[string]any) (*plugin.Response, err // --- RateBasedRule --- func (p *Provider) createRateBasedRule(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } - metricName := strParam(params, "MetricName") + metricName := shared.StrParam(params, "MetricName") if metricName == "" { metricName = name } @@ -823,7 +810,7 @@ func (p *Provider) createRateBasedRule(params map[string]any) (*plugin.Response, MetricName: metricName, RateLimit: rateLimit, Predicates: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateRateBasedRule(r); err != nil { return nil, err @@ -842,7 +829,7 @@ func (p *Provider) createRateBasedRule(params map[string]any) (*plugin.Response, } func (p *Provider) getRateBasedRule(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleId") + id := shared.StrParam(params, "RuleId") if id == "" { return wafError("WAFInvalidParameterException", "RuleId is required", http.StatusBadRequest), nil } @@ -883,7 +870,7 @@ func (p *Provider) listRateBasedRules(_ map[string]any) (*plugin.Response, error } func (p *Provider) updateRateBasedRule(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleId") + id := shared.StrParam(params, "RuleId") if id == "" { return wafError("WAFInvalidParameterException", "RuleId is required", http.StatusBadRequest), nil } @@ -897,7 +884,7 @@ func (p *Provider) updateRateBasedRule(params map[string]any) (*plugin.Response, if rl, ok := params["RateLimit"].(float64); ok { r.RateLimit = int64(rl) } - r.ChangeToken = strParam(params, "ChangeToken") + r.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateRateBasedRule(r); err != nil { return nil, err } @@ -905,7 +892,7 @@ func (p *Provider) updateRateBasedRule(params map[string]any) (*plugin.Response, } func (p *Provider) deleteRateBasedRule(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RuleId") + id := shared.StrParam(params, "RuleId") if id == "" { return wafError("WAFInvalidParameterException", "RuleId is required", http.StatusBadRequest), nil } @@ -921,7 +908,7 @@ func (p *Provider) deleteRateBasedRule(params map[string]any) (*plugin.Response, // --- ByteMatchSet --- func (p *Provider) createByteMatchSet(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } @@ -929,7 +916,7 @@ func (p *Provider) createByteMatchSet(params map[string]any) (*plugin.Response, b := &ByteMatchSet{ ID: id, Name: name, - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), Tuples: "[]", } if err := p.store.CreateByteMatchSet(b); err != nil { @@ -946,7 +933,7 @@ func (p *Provider) createByteMatchSet(params map[string]any) (*plugin.Response, } func (p *Provider) getByteMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "ByteMatchSetId") + id := shared.StrParam(params, "ByteMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "ByteMatchSetId is required", http.StatusBadRequest), nil } @@ -984,7 +971,7 @@ func (p *Provider) listByteMatchSets(_ map[string]any) (*plugin.Response, error) } func (p *Provider) updateByteMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "ByteMatchSetId") + id := shared.StrParam(params, "ByteMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "ByteMatchSetId is required", http.StatusBadRequest), nil } @@ -1013,7 +1000,7 @@ func (p *Provider) updateByteMatchSet(params map[string]any) (*plugin.Response, } b.Tuples = marshalJSON(tuples) } - b.ChangeToken = strParam(params, "ChangeToken") + b.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateByteMatchSet(b); err != nil { return nil, err } @@ -1021,7 +1008,7 @@ func (p *Provider) updateByteMatchSet(params map[string]any) (*plugin.Response, } func (p *Provider) deleteByteMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "ByteMatchSetId") + id := shared.StrParam(params, "ByteMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "ByteMatchSetId is required", http.StatusBadRequest), nil } @@ -1037,7 +1024,7 @@ func (p *Provider) deleteByteMatchSet(params map[string]any) (*plugin.Response, // --- RegexPatternSet --- func (p *Provider) createRegexPatternSet(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } @@ -1046,7 +1033,7 @@ func (p *Provider) createRegexPatternSet(params map[string]any) (*plugin.Respons ID: id, Name: name, Patterns: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateRegexPatternSet(r); err != nil { return nil, err @@ -1062,7 +1049,7 @@ func (p *Provider) createRegexPatternSet(params map[string]any) (*plugin.Respons } func (p *Provider) getRegexPatternSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RegexPatternSetId") + id := shared.StrParam(params, "RegexPatternSetId") if id == "" { return wafError("WAFInvalidParameterException", "RegexPatternSetId is required", http.StatusBadRequest), nil } @@ -1100,7 +1087,7 @@ func (p *Provider) listRegexPatternSets(_ map[string]any) (*plugin.Response, err } func (p *Provider) updateRegexPatternSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RegexPatternSetId") + id := shared.StrParam(params, "RegexPatternSetId") if id == "" { return wafError("WAFInvalidParameterException", "RegexPatternSetId is required", http.StatusBadRequest), nil } @@ -1129,7 +1116,7 @@ func (p *Provider) updateRegexPatternSet(params map[string]any) (*plugin.Respons } r.Patterns = marshalJSON(patterns) } - r.ChangeToken = strParam(params, "ChangeToken") + r.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateRegexPatternSet(r); err != nil { return nil, err } @@ -1137,7 +1124,7 @@ func (p *Provider) updateRegexPatternSet(params map[string]any) (*plugin.Respons } func (p *Provider) deleteRegexPatternSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RegexPatternSetId") + id := shared.StrParam(params, "RegexPatternSetId") if id == "" { return wafError("WAFInvalidParameterException", "RegexPatternSetId is required", http.StatusBadRequest), nil } @@ -1153,7 +1140,7 @@ func (p *Provider) deleteRegexPatternSet(params map[string]any) (*plugin.Respons // --- SizeConstraintSet --- func (p *Provider) createSizeConstraintSet(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } @@ -1162,7 +1149,7 @@ func (p *Provider) createSizeConstraintSet(params map[string]any) (*plugin.Respo ID: id, Name: name, Constraints: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateSizeConstraintSet(sc); err != nil { return nil, err @@ -1178,7 +1165,7 @@ func (p *Provider) createSizeConstraintSet(params map[string]any) (*plugin.Respo } func (p *Provider) getSizeConstraintSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "SizeConstraintSetId") + id := shared.StrParam(params, "SizeConstraintSetId") if id == "" { return wafError("WAFInvalidParameterException", "SizeConstraintSetId is required", http.StatusBadRequest), nil } @@ -1216,7 +1203,7 @@ func (p *Provider) listSizeConstraintSets(_ map[string]any) (*plugin.Response, e } func (p *Provider) updateSizeConstraintSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "SizeConstraintSetId") + id := shared.StrParam(params, "SizeConstraintSetId") if id == "" { return wafError("WAFInvalidParameterException", "SizeConstraintSetId is required", http.StatusBadRequest), nil } @@ -1245,7 +1232,7 @@ func (p *Provider) updateSizeConstraintSet(params map[string]any) (*plugin.Respo } sc.Constraints = marshalJSON(constraints) } - sc.ChangeToken = strParam(params, "ChangeToken") + sc.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateSizeConstraintSet(sc); err != nil { return nil, err } @@ -1253,7 +1240,7 @@ func (p *Provider) updateSizeConstraintSet(params map[string]any) (*plugin.Respo } func (p *Provider) deleteSizeConstraintSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "SizeConstraintSetId") + id := shared.StrParam(params, "SizeConstraintSetId") if id == "" { return wafError("WAFInvalidParameterException", "SizeConstraintSetId is required", http.StatusBadRequest), nil } @@ -1269,7 +1256,7 @@ func (p *Provider) deleteSizeConstraintSet(params map[string]any) (*plugin.Respo // --- SqlInjectionMatchSet --- func (p *Provider) createSqlInjectionMatchSet(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } @@ -1278,7 +1265,7 @@ func (p *Provider) createSqlInjectionMatchSet(params map[string]any) (*plugin.Re ID: id, Name: name, Tuples: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateSqlInjectionMatchSet(si); err != nil { return nil, err @@ -1294,7 +1281,7 @@ func (p *Provider) createSqlInjectionMatchSet(params map[string]any) (*plugin.Re } func (p *Provider) getSqlInjectionMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "SqlInjectionMatchSetId") + id := shared.StrParam(params, "SqlInjectionMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "SqlInjectionMatchSetId is required", http.StatusBadRequest), nil } @@ -1332,7 +1319,7 @@ func (p *Provider) listSqlInjectionMatchSets(_ map[string]any) (*plugin.Response } func (p *Provider) updateSqlInjectionMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "SqlInjectionMatchSetId") + id := shared.StrParam(params, "SqlInjectionMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "SqlInjectionMatchSetId is required", http.StatusBadRequest), nil } @@ -1361,7 +1348,7 @@ func (p *Provider) updateSqlInjectionMatchSet(params map[string]any) (*plugin.Re } si.Tuples = marshalJSON(tuples) } - si.ChangeToken = strParam(params, "ChangeToken") + si.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateSqlInjectionMatchSet(si); err != nil { return nil, err } @@ -1369,7 +1356,7 @@ func (p *Provider) updateSqlInjectionMatchSet(params map[string]any) (*plugin.Re } func (p *Provider) deleteSqlInjectionMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "SqlInjectionMatchSetId") + id := shared.StrParam(params, "SqlInjectionMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "SqlInjectionMatchSetId is required", http.StatusBadRequest), nil } @@ -1385,7 +1372,7 @@ func (p *Provider) deleteSqlInjectionMatchSet(params map[string]any) (*plugin.Re // --- XssMatchSet --- func (p *Provider) createXssMatchSet(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } @@ -1394,7 +1381,7 @@ func (p *Provider) createXssMatchSet(params map[string]any) (*plugin.Response, e ID: id, Name: name, Tuples: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateXssMatchSet(x); err != nil { return nil, err @@ -1410,7 +1397,7 @@ func (p *Provider) createXssMatchSet(params map[string]any) (*plugin.Response, e } func (p *Provider) getXssMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "XssMatchSetId") + id := shared.StrParam(params, "XssMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "XssMatchSetId is required", http.StatusBadRequest), nil } @@ -1448,7 +1435,7 @@ func (p *Provider) listXssMatchSets(_ map[string]any) (*plugin.Response, error) } func (p *Provider) updateXssMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "XssMatchSetId") + id := shared.StrParam(params, "XssMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "XssMatchSetId is required", http.StatusBadRequest), nil } @@ -1477,7 +1464,7 @@ func (p *Provider) updateXssMatchSet(params map[string]any) (*plugin.Response, e } x.Tuples = marshalJSON(tuples) } - x.ChangeToken = strParam(params, "ChangeToken") + x.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateXssMatchSet(x); err != nil { return nil, err } @@ -1485,7 +1472,7 @@ func (p *Provider) updateXssMatchSet(params map[string]any) (*plugin.Response, e } func (p *Provider) deleteXssMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "XssMatchSetId") + id := shared.StrParam(params, "XssMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "XssMatchSetId is required", http.StatusBadRequest), nil } @@ -1501,7 +1488,7 @@ func (p *Provider) deleteXssMatchSet(params map[string]any) (*plugin.Response, e // --- GeoMatchSet --- func (p *Provider) createGeoMatchSet(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } @@ -1510,7 +1497,7 @@ func (p *Provider) createGeoMatchSet(params map[string]any) (*plugin.Response, e ID: id, Name: name, Constraints: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateGeoMatchSet(g); err != nil { return nil, err @@ -1526,7 +1513,7 @@ func (p *Provider) createGeoMatchSet(params map[string]any) (*plugin.Response, e } func (p *Provider) getGeoMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "GeoMatchSetId") + id := shared.StrParam(params, "GeoMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "GeoMatchSetId is required", http.StatusBadRequest), nil } @@ -1564,7 +1551,7 @@ func (p *Provider) listGeoMatchSets(_ map[string]any) (*plugin.Response, error) } func (p *Provider) updateGeoMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "GeoMatchSetId") + id := shared.StrParam(params, "GeoMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "GeoMatchSetId is required", http.StatusBadRequest), nil } @@ -1593,7 +1580,7 @@ func (p *Provider) updateGeoMatchSet(params map[string]any) (*plugin.Response, e } g.Constraints = marshalJSON(constraints) } - g.ChangeToken = strParam(params, "ChangeToken") + g.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateGeoMatchSet(g); err != nil { return nil, err } @@ -1601,7 +1588,7 @@ func (p *Provider) updateGeoMatchSet(params map[string]any) (*plugin.Response, e } func (p *Provider) deleteGeoMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "GeoMatchSetId") + id := shared.StrParam(params, "GeoMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "GeoMatchSetId is required", http.StatusBadRequest), nil } @@ -1617,7 +1604,7 @@ func (p *Provider) deleteGeoMatchSet(params map[string]any) (*plugin.Response, e // --- RegexMatchSet --- func (p *Provider) createRegexMatchSet(params map[string]any) (*plugin.Response, error) { - name := strParam(params, "Name") + name := shared.StrParam(params, "Name") if name == "" { return wafError("WAFInvalidParameterException", "Name is required", http.StatusBadRequest), nil } @@ -1626,7 +1613,7 @@ func (p *Provider) createRegexMatchSet(params map[string]any) (*plugin.Response, ID: id, Name: name, Tuples: "[]", - ChangeToken: strParam(params, "ChangeToken"), + ChangeToken: shared.StrParam(params, "ChangeToken"), } if err := p.store.CreateRegexMatchSet(r); err != nil { return nil, err @@ -1642,7 +1629,7 @@ func (p *Provider) createRegexMatchSet(params map[string]any) (*plugin.Response, } func (p *Provider) getRegexMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RegexMatchSetId") + id := shared.StrParam(params, "RegexMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "RegexMatchSetId is required", http.StatusBadRequest), nil } @@ -1680,7 +1667,7 @@ func (p *Provider) listRegexMatchSets(_ map[string]any) (*plugin.Response, error } func (p *Provider) updateRegexMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RegexMatchSetId") + id := shared.StrParam(params, "RegexMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "RegexMatchSetId is required", http.StatusBadRequest), nil } @@ -1709,7 +1696,7 @@ func (p *Provider) updateRegexMatchSet(params map[string]any) (*plugin.Response, } r.Tuples = marshalJSON(tuples) } - r.ChangeToken = strParam(params, "ChangeToken") + r.ChangeToken = shared.StrParam(params, "ChangeToken") if err := p.store.UpdateRegexMatchSet(r); err != nil { return nil, err } @@ -1717,7 +1704,7 @@ func (p *Provider) updateRegexMatchSet(params map[string]any) (*plugin.Response, } func (p *Provider) deleteRegexMatchSet(params map[string]any) (*plugin.Response, error) { - id := strParam(params, "RegexMatchSetId") + id := shared.StrParam(params, "RegexMatchSetId") if id == "" { return wafError("WAFInvalidParameterException", "RegexMatchSetId is required", http.StatusBadRequest), nil } @@ -1749,7 +1736,7 @@ func (p *Provider) putLoggingConfiguration(params map[string]any) (*plugin.Respo } func (p *Provider) getLoggingConfiguration(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") if arn == "" { return wafError("WAFInvalidParameterException", "ResourceArn is required", http.StatusBadRequest), nil } @@ -1766,7 +1753,7 @@ func (p *Provider) getLoggingConfiguration(params map[string]any) (*plugin.Respo } func (p *Provider) deleteLoggingConfiguration(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") if arn == "" { return wafError("WAFInvalidParameterException", "ResourceArn is required", http.StatusBadRequest), nil } @@ -1796,8 +1783,8 @@ func (p *Provider) listLoggingConfigurations(_ map[string]any) (*plugin.Response // --- Permission Policy --- func (p *Provider) putPermissionPolicy(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") - policy := strParam(params, "Policy") + arn := shared.StrParam(params, "ResourceArn") + policy := shared.StrParam(params, "Policy") if arn == "" { return wafError("WAFInvalidParameterException", "ResourceArn is required", http.StatusBadRequest), nil } @@ -1808,7 +1795,7 @@ func (p *Provider) putPermissionPolicy(params map[string]any) (*plugin.Response, } func (p *Provider) getPermissionPolicy(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") if arn == "" { return wafError("WAFInvalidParameterException", "ResourceArn is required", http.StatusBadRequest), nil } @@ -1823,7 +1810,7 @@ func (p *Provider) getPermissionPolicy(params map[string]any) (*plugin.Response, } func (p *Provider) deletePermissionPolicy(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceArn") + arn := shared.StrParam(params, "ResourceArn") if arn == "" { return wafError("WAFInvalidParameterException", "ResourceArn is required", http.StatusBadRequest), nil } @@ -1839,7 +1826,7 @@ func (p *Provider) deletePermissionPolicy(params map[string]any) (*plugin.Respon // --- Tags --- func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceARN") + arn := shared.StrParam(params, "ResourceARN") if arn == "" { return wafError("WAFInvalidParameterException", "ResourceARN is required", http.StatusBadRequest), nil } @@ -1851,7 +1838,7 @@ func (p *Provider) tagResource(params map[string]any) (*plugin.Response, error) } func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceARN") + arn := shared.StrParam(params, "ResourceARN") if arn == "" { return wafError("WAFInvalidParameterException", "ResourceARN is required", http.StatusBadRequest), nil } @@ -1863,7 +1850,7 @@ func (p *Provider) untagResource(params map[string]any) (*plugin.Response, error } func (p *Provider) listTagsForResource(params map[string]any) (*plugin.Response, error) { - arn := strParam(params, "ResourceARN") + arn := shared.StrParam(params, "ResourceARN") if arn == "" { return wafError("WAFInvalidParameterException", "ResourceARN is required", http.StatusBadRequest), nil } diff --git a/internal/services/wafv2/provider.go b/internal/services/wafv2/provider.go index e5d9ea57..4457c8e3 100644 --- a/internal/services/wafv2/provider.go +++ b/internal/services/wafv2/provider.go @@ -214,10 +214,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- WebACL --- func (p *Provider) createWebACL(params map[string]any) (*plugin.Response, error) { diff --git a/internal/services/xray/provider.go b/internal/services/xray/provider.go index 4e7bc798..1abf2437 100644 --- a/internal/services/xray/provider.go +++ b/internal/services/xray/provider.go @@ -264,10 +264,6 @@ func (p *Provider) ListResources(_ context.Context) ([]plugin.Resource, error) { return res, nil } -func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} - // --- Group --- func (p *Provider) createGroup(params map[string]any) (*plugin.Response, error) { diff --git a/internal/shared/arn.go b/internal/shared/arn.go index 081196d3..ffdc96cb 100644 --- a/internal/shared/arn.go +++ b/internal/shared/arn.go @@ -2,11 +2,17 @@ package shared -import "fmt" +import ( + "fmt" + + "github.com/skyoo2003/devcloud/internal/plugin" +) const ( - DefaultRegion = "us-east-1" - DefaultAccountID = "000000000000" + DefaultRegion = "us-east-1" + // DefaultAccountID mirrors plugin.DefaultAccountID so ARN builders and + // providers cannot drift apart. + DefaultAccountID = plugin.DefaultAccountID ) func BuildARN(service, resourceType, resourceID string) string { diff --git a/internal/shared/params.go b/internal/shared/params.go new file mode 100644 index 00000000..c77ffb27 --- /dev/null +++ b/internal/shared/params.go @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 + +package shared + +// StrParam returns params[key] as a string, or "" when the key is absent or +// holds another type. Providers parse request bodies into map[string]any, so +// this is the standard accessor for a string field. +func StrParam(params map[string]any, key string) string { + s, _ := params[key].(string) + return s +} diff --git a/internal/shared/response.go b/internal/shared/response.go index 6c2a893f..ee5fc003 100644 --- a/internal/shared/response.go +++ b/internal/shared/response.go @@ -5,8 +5,6 @@ package shared import ( "encoding/json" "encoding/xml" - "strings" - "unicode" "github.com/skyoo2003/devcloud/internal/plugin" ) @@ -49,94 +47,3 @@ func QueryXMLError(code, message string, status int) *plugin.Response { b, _ := xml.Marshal(e) return &plugin.Response{StatusCode: status, Body: b, ContentType: "text/xml"} } - -// ToCamelCase converts a PascalCase string to camelCase. -// E.g. "ClusterArn" -> "clusterArn", "ARN" -> "arn", "HTTPStatusCode" -> "httpStatusCode". -func ToCamelCase(s string) string { - if s == "" { - return s - } - // Find the first lowercase letter or end of leading uppercase run - runes := []rune(s) - i := 0 - for i < len(runes) && unicode.IsUpper(runes[i]) { - i++ - } - if i == 0 { - return s // already camelCase - } - if i == 1 { - // Single uppercase letter at start - return strings.ToLower(string(runes[0])) + string(runes[1:]) - } - // Multiple uppercase: e.g. "ARN" -> "arn", "ARNPrefix" -> "arnPrefix" - if i == len(runes) { - return strings.ToLower(s) - } - return strings.ToLower(string(runes[:i-1])) + string(runes[i-1:]) -} - -// CamelCaseKeys recursively converts all map keys from PascalCase to camelCase. -func CamelCaseKeys(v any) any { - switch val := v.(type) { - case map[string]any: - result := make(map[string]any, len(val)) - for k, v := range val { - result[ToCamelCase(k)] = CamelCaseKeys(v) - } - return result - case []any: - result := make([]any, len(val)) - for i, item := range val { - result[i] = CamelCaseKeys(item) - } - return result - case []map[string]any: - result := make([]any, len(val)) - for i, item := range val { - result[i] = CamelCaseKeys(item) - } - return result - default: - return v - } -} - -// ToPascalCase converts a camelCase string to PascalCase. -func ToPascalCase(s string) string { - if s == "" { - return s - } - runes := []rune(s) - runes[0] = unicode.ToUpper(runes[0]) - return string(runes) -} - -// PascalCaseKeys recursively converts all map keys from camelCase to PascalCase. -func PascalCaseKeys(v any) map[string]any { - m, ok := v.(map[string]any) - if !ok { - return nil - } - result := make(map[string]any, len(m)) - for k, val := range m { - newKey := ToPascalCase(k) - switch typedVal := val.(type) { - case map[string]any: - result[newKey] = PascalCaseKeys(typedVal) - case []any: - newSlice := make([]any, len(typedVal)) - for i, item := range typedVal { - if subMap, ok := item.(map[string]any); ok { - newSlice[i] = PascalCaseKeys(subMap) - } else { - newSlice[i] = item - } - } - result[newKey] = newSlice - default: - result[newKey] = val - } - } - return result -} diff --git a/internal/shared/store.go b/internal/shared/store.go deleted file mode 100644 index 3692fb7a..00000000 --- a/internal/shared/store.go +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package shared - -import ( - "fmt" - "strings" - - "github.com/skyoo2003/devcloud/internal/storage/sqlite" -) - -type Scanner interface { - Scan(dest ...any) error -} - -type ResourceStore[T any] struct { - db *sqlite.Store - table string - idCol string - cols string - scanner func(Scanner) (T, error) -} - -func NewResourceStore[T any](db *sqlite.Store, table, idCol, cols string, scanner func(Scanner) (T, error)) (*ResourceStore[T], error) { - table = strings.TrimSpace(table) - idCol = strings.TrimSpace(idCol) - if err := validateIdentifier(table, "table"); err != nil { - return nil, err - } - if err := validateIdentifier(idCol, "idCol"); err != nil { - return nil, err - } - var validCols []string - for _, c := range strings.Split(cols, ",") { - c = strings.TrimSpace(c) - if c == "" { - continue - } - if err := validateIdentifier(c, "col"); err != nil { - return nil, err - } - validCols = append(validCols, c) - } - normalizedCols := strings.Join(validCols, ", ") - return &ResourceStore[T]{db: db, table: table, idCol: idCol, cols: normalizedCols, scanner: scanner}, nil -} - -func validateIdentifier(s, kind string) error { - if len(s) == 0 { - return fmt.Errorf("shared: empty %s identifier", kind) - } - for _, r := range s { - isLower := r >= 'a' && r <= 'z' - isUpper := r >= 'A' && r <= 'Z' - isDigit := r >= '0' && r <= '9' - isUnderscore := r == '_' - if !isLower && !isUpper && !isDigit && !isUnderscore { - return fmt.Errorf("shared: invalid %s identifier: %q", kind, s) - } - } - return nil -} - -func (s *ResourceStore[T]) DB() *sqlite.Store { return s.db } - -func (s *ResourceStore[T]) Get(id string) (T, error) { - row := s.db.DB().QueryRow("SELECT "+s.cols+" FROM "+s.table+" WHERE "+s.idCol+" = ?", id) - return s.scanner(row) -} - -func (s *ResourceStore[T]) List(where string, args []any) ([]T, error) { - query := "SELECT " + s.cols + " FROM " + s.table - if where != "" { - query += " WHERE " + where - } - rows, err := s.db.DB().Query(query, args...) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - var items []T - for rows.Next() { - item, err := s.scanner(rows) - if err != nil { - return nil, err - } - items = append(items, item) - } - return items, rows.Err() -} - -func (s *ResourceStore[T]) Delete(id string) (bool, error) { - res, err := s.db.DB().Exec("DELETE FROM "+s.table+" WHERE "+s.idCol+" = ?", id) - if err != nil { - return false, err - } - n, _ := res.RowsAffected() - return n > 0, nil -} - -func (s *ResourceStore[T]) Count(where string, args []any) (int, error) { - query := "SELECT COUNT(*) FROM " + s.table - if where != "" { - query += " WHERE " + where - } - var n int - err := s.db.DB().QueryRow(query, args...).Scan(&n) - return n, err -} - -func (s *ResourceStore[T]) Exec(query string, args ...any) error { - _, err := s.db.DB().Exec(query, args...) - return err -} diff --git a/internal/shared/store_test.go b/internal/shared/store_test.go deleted file mode 100644 index 31b9d789..00000000 --- a/internal/shared/store_test.go +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package shared - -import ( - "database/sql" - "errors" - "testing" - - "github.com/skyoo2003/devcloud/internal/storage/sqlite" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type testItem struct { - ID string - Name string -} - -var testMigrations = []sqlite.Migration{ - {Version: 1, SQL: ` - CREATE TABLE items ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL - ); - `}, -} - -func testScanner(s Scanner) (testItem, error) { - var item testItem - err := s.Scan(&item.ID, &item.Name) - return item, err -} - -func newTestResourceStore(t *testing.T) *ResourceStore[testItem] { - t.Helper() - dbPath := t.TempDir() + "/test.db" - db, err := sqlite.Open(dbPath, testMigrations) - require.NoError(t, err) - t.Cleanup(func() { _ = db.Close() }) - rs, err := NewResourceStore[testItem](db, "items", "id", "id, name", testScanner) - require.NoError(t, err) - return rs -} - -func TestResourceStore_GetNotFound(t *testing.T) { - rs := newTestResourceStore(t) - _, err := rs.Get("nonexistent") - assert.True(t, errors.Is(err, sql.ErrNoRows)) -} - -func TestResourceStore_InsertAndGet(t *testing.T) { - rs := newTestResourceStore(t) - require.NoError(t, rs.Exec("INSERT INTO items (id, name) VALUES (?, ?)", "1", "alpha")) - - item, err := rs.Get("1") - require.NoError(t, err) - assert.Equal(t, "1", item.ID) - assert.Equal(t, "alpha", item.Name) -} - -func TestResourceStore_List(t *testing.T) { - rs := newTestResourceStore(t) - require.NoError(t, rs.Exec("INSERT INTO items (id, name) VALUES (?, ?)", "1", "alpha")) - require.NoError(t, rs.Exec("INSERT INTO items (id, name) VALUES (?, ?)", "2", "beta")) - - items, err := rs.List("", nil) - require.NoError(t, err) - assert.Len(t, items, 2) -} - -func TestResourceStore_ListWithFilter(t *testing.T) { - rs := newTestResourceStore(t) - require.NoError(t, rs.Exec("INSERT INTO items (id, name) VALUES (?, ?)", "1", "alpha")) - require.NoError(t, rs.Exec("INSERT INTO items (id, name) VALUES (?, ?)", "2", "beta")) - - items, err := rs.List("name = ?", []any{"beta"}) - require.NoError(t, err) - require.Len(t, items, 1) - assert.Equal(t, "beta", items[0].Name) -} - -func TestResourceStore_Delete(t *testing.T) { - rs := newTestResourceStore(t) - require.NoError(t, rs.Exec("INSERT INTO items (id, name) VALUES (?, ?)", "1", "alpha")) - - deleted, err := rs.Delete("1") - require.NoError(t, err) - assert.True(t, deleted) - - _, err = rs.Get("1") - assert.True(t, errors.Is(err, sql.ErrNoRows)) -} - -func TestResourceStore_DeleteNotFound(t *testing.T) { - rs := newTestResourceStore(t) - deleted, err := rs.Delete("nonexistent") - require.NoError(t, err) - assert.False(t, deleted) -} - -func TestResourceStore_Count(t *testing.T) { - rs := newTestResourceStore(t) - require.NoError(t, rs.Exec("INSERT INTO items (id, name) VALUES (?, ?)", "1", "alpha")) - require.NoError(t, rs.Exec("INSERT INTO items (id, name) VALUES (?, ?)", "2", "beta")) - - n, err := rs.Count("", nil) - require.NoError(t, err) - assert.Equal(t, 2, n) - - n, err = rs.Count("name = ?", []any{"alpha"}) - require.NoError(t, err) - assert.Equal(t, 1, n) -} - -func newTestDB(t *testing.T) *sqlite.Store { - t.Helper() - dbPath := t.TempDir() + "/test.db" - db, err := sqlite.Open(dbPath, testMigrations) - require.NoError(t, err) - t.Cleanup(func() { _ = db.Close() }) - return db -} - -func TestNewResourceStore_InvalidTable(t *testing.T) { - db := newTestDB(t) - _, err := NewResourceStore[testItem](db, "DROP TABLE items; --", "id", "id", testScanner) - assert.ErrorContains(t, err, "invalid table identifier") -} - -func TestNewResourceStore_InvalidIdCol(t *testing.T) { - db := newTestDB(t) - _, err := NewResourceStore[testItem](db, "items", "id; --", "id", testScanner) - assert.ErrorContains(t, err, "invalid idCol identifier") -} - -func TestNewResourceStore_InvalidCol(t *testing.T) { - db := newTestDB(t) - _, err := NewResourceStore[testItem](db, "items", "id", "id, name; --", testScanner) - assert.ErrorContains(t, err, "invalid col identifier") -} - -func TestNewResourceStore_EmptyTable(t *testing.T) { - db := newTestDB(t) - _, err := NewResourceStore[testItem](db, "", "id", "id", testScanner) - assert.ErrorContains(t, err, "empty table identifier") -} - -func TestNewResourceStore_TrailingComma(t *testing.T) { - db := newTestDB(t) - rs, err := NewResourceStore[testItem](db, "items", "id", "id, name,", testScanner) - require.NoError(t, err) - require.NotNil(t, rs) -} - -func TestNewResourceStore_ValidIdentifiers(t *testing.T) { - db := newTestDB(t) - - tests := []struct { - name string - tableName string - primary string - cols string - }{ - { - name: "underscores", - tableName: "items", - primary: "id", - cols: "id, item_name, created_at", - }, - { - name: "digits_in_identifiers", - tableName: "items", - primary: "id", - cols: "id, name2, col3_v1", - }, - { - name: "trailing_comma", - tableName: "items", - primary: "id", - cols: "id, name,", - }, - { - name: "whitespace_in_identifiers", - tableName: " items ", - primary: " id ", - cols: " id, name ", - }, - { - name: "leading_comma_in_cols", - tableName: "items", - primary: "id", - cols: ", id, name", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - rs, err := NewResourceStore[testItem](db, tc.tableName, tc.primary, tc.cols, testScanner) - require.NoError(t, err) - require.NotNil(t, rs) - }) - } -} From 9d899a47aa6c61afb2ba024ba13c3750eb9983c8 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Thu, 30 Jul 2026 08:00:00 +0900 Subject: [PATCH 2/8] refactor: simplify startup, config, and the admin log collector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embedded default config was 325 lines in which all 103 service entries were byte-identical boilerplate: enabled plus data_dir ./data/, with no exceptions. Derive that instead. The services block is now optional and authoritative — omit it and every registered service starts under ./data/; list any service and only the listed ones start, which is the behaviour existing partial configs already relied on. Startup iterates the plugin registry rather than a config map, so a newly registered service no longer needs a YAML entry to run. Delete the event bus and the admin WebSocket at /devcloud/api/ws: nothing in the binary ever called Publish, and main.go dropped the bus reference at the call site, so the socket accepted connections and could never send a message. The REST /devcloud/api/logs endpoint already serves the request log it was meant to stream. This drops the gorilla/websocket dependency. Replace the custom buffering slog.Handler (92 lines) with config warnings returned as a []string and logged after setupLogging — same guarantee that config-time warnings honour logging.format/level, without a bespoke handler. The 'config file not found, using embedded defaults' notice is gone: it fired on the zero-config happy path, and keeping it was the only reason the handler existed. Fold the two init loops, whose bodies were identical, into one closure. The fixed initOrder stays: it encodes core-service-failure-is-fatal, not just the iam-before-sts ordering. Keep the deprecated 'dashboard' key shim — the rename it protects is still unreleased, so dropping it now would silently break v0.2.0 configs. Drop two of the LogCollector's three redundant size clamps, keeping the divide-by-zero guard. --- cmd/devcloud/buffer.go | 43 ---- cmd/devcloud/buffer_test.go | 49 ----- cmd/devcloud/main.go | 94 ++++----- go.mod | 1 - go.sum | 2 - internal/admin/logger.go | 17 +- internal/admin/websocket.go | 142 ------------- internal/admin/websocket_test.go | 96 --------- internal/config/config.go | 114 ++++++---- internal/config/config_test.go | 150 ++++++++------ internal/config/default.yaml | 323 +---------------------------- internal/eventbus/eventbus.go | 81 -------- internal/eventbus/eventbus_test.go | 84 -------- 13 files changed, 200 insertions(+), 996 deletions(-) delete mode 100644 cmd/devcloud/buffer.go delete mode 100644 cmd/devcloud/buffer_test.go delete mode 100644 internal/admin/websocket.go delete mode 100644 internal/admin/websocket_test.go delete mode 100644 internal/eventbus/eventbus.go delete mode 100644 internal/eventbus/eventbus_test.go diff --git a/cmd/devcloud/buffer.go b/cmd/devcloud/buffer.go deleted file mode 100644 index 2399c32b..00000000 --- a/cmd/devcloud/buffer.go +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "log/slog" -) - -// bufferHandler is a slog.Handler that captures every record instead of -// emitting it, so log calls made before the operator-configured handler is -// installed (notably config.parse() warnings) can be replayed afterwards and -// honor logging.format / logging.level. See flushTo. -// -// ponytail: single-threaded startup, add a mutex if buffered logging ever -// goes concurrent. -type bufferHandler struct { - records []slog.Record -} - -// Enabled buffers all levels; the real handler filters on replay in flushTo. -func (h *bufferHandler) Enabled(context.Context, slog.Level) bool { return true } - -func (h *bufferHandler) Handle(_ context.Context, r slog.Record) error { - h.records = append(h.records, r.Clone()) - return nil -} - -// WithAttrs/WithGroup return the handler unchanged: config-time logging uses -// plain slog.Warn/Info without attr or group chaining, so nothing is lost. -func (h *bufferHandler) WithAttrs([]slog.Attr) slog.Handler { return h } -func (h *bufferHandler) WithGroup(string) slog.Handler { return h } - -// flushTo replays buffered records through dst, honoring dst's level filter, -// then clears the buffer. -func (h *bufferHandler) flushTo(dst slog.Handler) { - for _, r := range h.records { - if dst.Enabled(context.Background(), r.Level) { - _ = dst.Handle(context.Background(), r) - } - } - h.records = nil -} diff --git a/cmd/devcloud/buffer_test.go b/cmd/devcloud/buffer_test.go deleted file mode 100644 index ee5e036a..00000000 --- a/cmd/devcloud/buffer_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "bytes" - "context" - "log/slog" - "strings" - "testing" -) - -// TestBufferHandler_FlushHonorsFormatAndLevel verifies that records buffered -// before the real handler is installed are replayed as JSON and filtered by -// the destination handler's level — the two acceptance criteria from #113. -func TestBufferHandler_FlushHonorsFormatAndLevel(t *testing.T) { - buf := &bufferHandler{} - log := slog.New(buf) - log.Info("config file not found") - log.Warn("dashboard key deprecated", "token", "tierX") - - var out bytes.Buffer - dst := slog.NewJSONHandler(&out, &slog.HandlerOptions{Level: slog.LevelWarn}) - buf.flushTo(dst) - - got := out.String() - if strings.Contains(got, "config file not found") { - t.Errorf("info record should be dropped at level=warn, got: %s", got) - } - if !strings.Contains(got, `"dashboard key deprecated"`) || !strings.Contains(got, `"token":"tierX"`) { - t.Errorf("warn record (with attrs) should be replayed as JSON, got: %s", got) - } - - // flush clears the buffer so a second flush is a no-op. - out.Reset() - buf.flushTo(dst) - if out.Len() != 0 { - t.Errorf("second flush should emit nothing, got: %s", out.String()) - } -} - -// TestBufferHandler_EnabledBuffersAllLevels confirms buffering does not drop -// records below the eventual threshold; the level filter applies only on flush. -func TestBufferHandler_EnabledBuffersAllLevels(t *testing.T) { - buf := &bufferHandler{} - if !buf.Enabled(context.Background(), slog.LevelDebug) { - t.Fatal("bufferHandler must buffer all levels, including debug") - } -} diff --git a/cmd/devcloud/main.go b/cmd/devcloud/main.go index 69dc9086..73b3433c 100644 --- a/cmd/devcloud/main.go +++ b/cmd/devcloud/main.go @@ -10,18 +10,19 @@ import ( "net/http" "os" "os/signal" - "sort" "syscall" "time" "github.com/skyoo2003/devcloud/internal/admin" "github.com/skyoo2003/devcloud/internal/config" - "github.com/skyoo2003/devcloud/internal/eventbus" "github.com/skyoo2003/devcloud/internal/gateway" "github.com/skyoo2003/devcloud/internal/plugin" iamsvc "github.com/skyoo2003/devcloud/internal/services/iam" ) +// initOrder lists the services that must come up for DevCloud to be useful, in +// dependency order — sts borrows iam's store, so iam precedes it. A failure +// here is fatal; every other registered service only warns. var initOrder = []string{ "s3", "sqs", "dynamodb", "iam", "sts", "lambda", "kms", "sns", "secretsmanager", "ssm", "cloudwatchlogs", "cloudwatch", @@ -32,89 +33,71 @@ func main() { cfgPath := flag.String("config", "", "Path to config file (optional; uses ./devcloud.yaml if present, else embedded defaults)") flag.Parse() - // Buffer log records emitted while loading config (parse() deprecation and - // unknown-tier warnings, etc.) so they can be replayed through the - // operator-configured handler below and honor logging.format / level. - buf := &bufferHandler{} - slog.SetDefault(slog.New(buf)) - var ( - cfg *config.Config - err error + cfg *config.Config + warnings []string + err error ) if *cfgPath != "" { // Explicit --config flag: the file must exist. - cfg, err = config.Load(*cfgPath) + cfg, warnings, err = config.Load(*cfgPath) } else { // No flag: prefer ./devcloud.yaml in the working directory; fall back to embedded defaults. - cfg, err = config.LoadOrDefault("devcloud.yaml") + cfg, warnings, err = config.LoadOrDefault("devcloud.yaml") } if err != nil { // Load failed, so there is no logging config to honor; fall back to the - // default handler, flush any buffered warnings, then surface the error. + // default handler and surface the error. setupLogging(config.LoggingConfig{}) - buf.flushTo(slog.Default().Handler()) slog.Error("failed to load config", "error", err) os.Exit(1) } + // Config warnings are returned rather than logged by the config package so + // they honor logging.format / logging.level. setupLogging(cfg.Logging) - buf.flushTo(slog.Default().Handler()) + for _, w := range warnings { + slog.Warn(w) + } registry := plugin.DefaultRegistry - // Initialize services in dependency order. - for _, name := range initOrder { - svcCfg, exists := cfg.Services[name] - if !exists || !svcCfg.Enabled { - continue + initService := func(name string, fatal bool) { + svcCfg := cfg.Service(name) + if !svcCfg.Enabled { + return } pluginCfg := plugin.PluginConfig{ DataDir: svcCfg.DataDir, Options: buildOptions(name, cfg, registry), } if _, err := registry.Init(name, pluginCfg); err != nil { - slog.Error("failed to init service", "service", name, "error", err) - os.Exit(1) + if fatal { + slog.Error("failed to init service", "service", name, "error", err) + os.Exit(1) + } + slog.Warn("service init failed", "service", name, "error", err) + return } slog.Info("service initialized", "service", name) } - // Initialize any remaining enabled services not in initOrder, in - // deterministic alphabetical order so startup logs and init sequencing - // are reproducible across runs. - remaining := make([]string, 0, len(cfg.Services)) - for name := range cfg.Services { - remaining = append(remaining, name) + for _, name := range initOrder { + initService(name, true) } - sort.Strings(remaining) - for _, name := range remaining { - svcCfg := cfg.Services[name] - if !svcCfg.Enabled { - continue - } + // RegisteredServices() is sorted, so the long tail starts in a reproducible + // order. Services already brought up above are skipped. + for _, name := range registry.RegisteredServices() { if _, ok := registry.Get(name); ok { continue } - pluginCfg := plugin.PluginConfig{ - DataDir: svcCfg.DataDir, - Options: buildOptions(name, cfg, registry), - } - if _, err := registry.Init(name, pluginCfg); err != nil { - slog.Warn("service init failed", "service", name, "error", err) - continue - } - slog.Info("service initialized", "service", name) - } - - if cfg.Auth.Enabled { - slog.Warn("auth.enabled=true but SigV4 enforcement is not yet implemented; requests are accepted regardless of signature validity") + initService(name, false) } - // Admin API: build the REST + WebSocket handler only when the operator - // opted in via admin.enabled. Otherwise expose a 404 handler so the admin - // routes don't leak service internals. This binary serves no web UI; the - // dashboard frontend lives in a separate repository and talks to this API. + // Admin API: build the REST handler only when the operator opted in via + // admin.enabled. Otherwise expose a 404 handler so the admin routes don't + // leak service internals. This binary serves no web UI; the dashboard + // frontend lives in a separate repository and talks to this API. // The log collector is only built when admin is enabled; otherwise no // consumer can ever read it, so a nil collector keeps Add off the request // hot path (see gateway.New). @@ -122,14 +105,7 @@ func main() { adminHandler := http.NotFoundHandler() if cfg.Admin.Enabled { logCollector = admin.NewLogCollector(1000) - adminAPI := admin.NewAPI(registry, logCollector) - hub := admin.NewHub(eventbus.New()) - go hub.Start() - - adminMux := http.NewServeMux() - adminMux.Handle("/devcloud/api/", adminAPI.Handler()) - adminMux.HandleFunc("/devcloud/api/ws", hub.ServeWS) - adminHandler = adminMux + adminHandler = admin.NewAPI(registry, logCollector).Handler() slog.Info("admin API enabled") } gw := gateway.New(cfg.Server.Port, registry, adminHandler, logCollector) diff --git a/go.mod b/go.mod index 125e0ff4..6284185c 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.26 toolchain go1.26.1 require ( - github.com/gorilla/websocket v1.5.3 github.com/mattn/go-sqlite3 v1.14.48 github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 087a5085..78445778 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= diff --git a/internal/admin/logger.go b/internal/admin/logger.go index 59c4a384..8ffa9622 100644 --- a/internal/admin/logger.go +++ b/internal/admin/logger.go @@ -7,11 +7,6 @@ import ( "time" ) -const ( - defaultLogCollectorSize = 1000 - maxLogCollectorSize = 10000 -) - // RequestLog holds details about a single API request. type RequestLog struct { Method string `json:"method"` @@ -32,14 +27,11 @@ type LogCollector struct { } // NewLogCollector creates a LogCollector that holds at most maxSize entries. +// A non-positive maxSize would make Add divide by zero, so it is floored at 1. func NewLogCollector(maxSize int) *LogCollector { - if maxSize <= 0 { - maxSize = defaultLogCollectorSize - } - if maxSize > maxLogCollectorSize { - maxSize = maxLogCollectorSize + if maxSize < 1 { + maxSize = 1 } - return &LogCollector{ entries: make([]RequestLog, maxSize), maxSize: maxSize, @@ -67,9 +59,6 @@ func (c *LogCollector) Recent(n int) []RequestLog { if n <= 0 || c.count == 0 { return []RequestLog{} } - if n > maxLogCollectorSize { - n = maxLogCollectorSize - } if n > c.count { n = c.count } diff --git a/internal/admin/websocket.go b/internal/admin/websocket.go deleted file mode 100644 index c494961b..00000000 --- a/internal/admin/websocket.go +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package admin - -import ( - "encoding/json" - "log" - "net/http" - "sync" - "time" - - "github.com/gorilla/websocket" - "github.com/skyoo2003/devcloud/internal/eventbus" -) - -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, -} - -// eventMessage is the JSON format sent to admin clients. -type eventMessage struct { - Source string `json:"source"` - Type string `json:"type"` - Detail map[string]any `json:"detail"` - Timestamp time.Time `json:"timestamp"` -} - -// Client represents a single connected WebSocket client. -type Client struct { - conn *websocket.Conn - send chan []byte -} - -// Hub maintains connected clients and broadcasts EventBus events to them. -type Hub struct { - mu sync.RWMutex - clients map[*Client]bool - bus eventbus.EventBus -} - -// NewHub creates a new Hub with a reference to the provided EventBus. -func NewHub(bus eventbus.EventBus) *Hub { - return &Hub{ - clients: make(map[*Client]bool), - bus: bus, - } -} - -// Start subscribes to all events on the EventBus and begins broadcasting them -// to connected WebSocket clients. -func (h *Hub) Start() { - h.bus.Subscribe("*", func(e eventbus.Event) { - msg := eventMessage{ - Source: e.Source, - Type: e.Type, - Detail: e.Detail, - Timestamp: e.Timestamp, - } - data, err := json.Marshal(msg) - if err != nil { - log.Printf("admin: failed to marshal event: %v", err) - return - } - h.broadcast(data) - }) -} - -// ServeWS upgrades an HTTP connection to WebSocket, registers the client with -// the hub, and starts its write pump. -func (h *Hub) ServeWS(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Printf("admin: websocket upgrade failed: %v", err) - return - } - - client := &Client{ - conn: conn, - send: make(chan []byte, 256), - } - - h.mu.Lock() - h.clients[client] = true - h.mu.Unlock() - - // cleanup ensures the client is unregistered and the connection is closed - // exactly once, regardless of which pump exits first. - var once sync.Once - cleanup := func() { - once.Do(func() { - h.mu.Lock() - delete(h.clients, client) - h.mu.Unlock() - _ = conn.Close() - }) - } - - // Write pump: forwards messages from the send channel to the WebSocket connection. - go func() { - defer cleanup() - for data := range client.send { - if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { - log.Printf("admin: write error: %v", err) - return - } - } - }() - - // Read pump: keeps the connection alive and detects disconnects. - go func() { - defer func() { - close(client.send) - cleanup() - }() - for { - if _, _, err := conn.ReadMessage(); err != nil { - break - } - } - }() -} - -// broadcast sends data to every connected client. -func (h *Hub) broadcast(data []byte) { - h.mu.RLock() - clients := make([]*Client, 0, len(h.clients)) - for c := range h.clients { - clients = append(clients, c) - } - h.mu.RUnlock() - - for _, c := range clients { - select { - case c.send <- data: - default: - // Drop message if the client's send buffer is full. - log.Printf("admin: client send buffer full, dropping message") - } - } -} diff --git a/internal/admin/websocket_test.go b/internal/admin/websocket_test.go deleted file mode 100644 index a870b8a7..00000000 --- a/internal/admin/websocket_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package admin - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/skyoo2003/devcloud/internal/eventbus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// wsURL converts an http:// URL to ws://. -func wsURL(serverURL string) string { - return "ws" + strings.TrimPrefix(serverURL, "http") -} - -// dialWS connects a gorilla WebSocket client to the given URL. -func dialWS(t *testing.T, url string) *websocket.Conn { - t.Helper() - dialer := websocket.Dialer{} - conn, _, err := dialer.Dial(url, nil) - require.NoError(t, err) - return conn -} - -// TestHub_ClientConnects verifies that a client can successfully upgrade to -// WebSocket and that the hub registers it. -func TestHub_ClientConnects(t *testing.T) { - bus := eventbus.New() - hub := NewHub(bus) - hub.Start() - - server := httptest.NewServer(http.HandlerFunc(hub.ServeWS)) - defer server.Close() - - conn := dialWS(t, wsURL(server.URL)) - defer func() { _ = conn.Close() }() - - // Give the hub a moment to register the client. - time.Sleep(50 * time.Millisecond) - - hub.mu.RLock() - clientCount := len(hub.clients) - hub.mu.RUnlock() - - assert.Equal(t, 1, clientCount, "expected 1 registered client after connection") -} - -// TestHub_BroadcastEvent verifies that an event published to the EventBus is -// delivered to a connected WebSocket client as the expected JSON payload. -func TestHub_BroadcastEvent(t *testing.T) { - bus := eventbus.New() - hub := NewHub(bus) - hub.Start() - - server := httptest.NewServer(http.HandlerFunc(hub.ServeWS)) - defer server.Close() - - conn := dialWS(t, wsURL(server.URL)) - defer func() { _ = conn.Close() }() - - // Allow the hub to register the client before publishing. - time.Sleep(50 * time.Millisecond) - - ts := time.Now().UTC().Truncate(time.Second) - evt := eventbus.Event{ - Source: "s3", - Type: "s3:ObjectCreated", - Detail: map[string]any{"key": "my-file.txt", "size": float64(1024)}, - Timestamp: ts, - } - err := bus.Publish(context.Background(), evt) - require.NoError(t, err) - - // Read message with a deadline to avoid hanging on failure. - _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - _, raw, err := conn.ReadMessage() - require.NoError(t, err) - - var got eventMessage - require.NoError(t, json.Unmarshal(raw, &got)) - - assert.Equal(t, "s3", got.Source) - assert.Equal(t, "s3:ObjectCreated", got.Type) - assert.Equal(t, "my-file.txt", got.Detail["key"]) - assert.Equal(t, float64(1024), got.Detail["size"]) - assert.WithinDuration(t, ts, got.Timestamp, time.Second) -} diff --git a/internal/config/config.go b/internal/config/config.go index d096fd2a..b04c983f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,7 +4,7 @@ package config import ( _ "embed" - "log/slog" + "fmt" "os" "path/filepath" "strconv" @@ -14,16 +14,19 @@ import ( ) // defaultConfigYAML is the built-in configuration used when no YAML file is -// provided. It enables all services with standard data directories so DevCloud -// runs out of the box with zero setup. +// provided. It carries no services block, which means "every registered +// service, enabled, under ./data/" — see Config.Service. // //go:embed default.yaml var defaultConfigYAML []byte +// defaultDataDir is the base directory services store data under when neither +// the YAML nor DEVCLOUD_DATA_DIR names one. +const defaultDataDir = "./data" + type Config struct { Server ServerConfig `yaml:"server"` Services map[string]ServiceConfig `yaml:"services"` - Auth AuthConfig `yaml:"auth"` Admin *AdminConfig `yaml:"admin"` Logging LoggingConfig `yaml:"logging"` @@ -32,6 +35,11 @@ type Config struct { // Both Admin and Dashboard are pointers so parse can tell an explicit // block from an absent one and apply the correct precedence. Dashboard *AdminConfig `yaml:"dashboard"` + + // allowed is the DEVCLOUD_SERVICES filter; nil means no filtering. + allowed map[string]bool + // baseDir is the DEVCLOUD_DATA_DIR override; "" means honour data_dir. + baseDir string } type ServerConfig struct { @@ -43,10 +51,6 @@ type ServiceConfig struct { DataDir string `yaml:"data_dir"` } -type AuthConfig struct { - Enabled bool `yaml:"enabled"` -} - type AdminConfig struct { Enabled bool `yaml:"enabled"` } @@ -56,12 +60,41 @@ type LoggingConfig struct { Format string `yaml:"format"` } -// Load reads and parses a YAML config file from the given path. -// Returns an error if the file cannot be read or parsed. -func Load(path string) (*Config, error) { +// Service returns the effective configuration for serviceID. +// +// A YAML services block is authoritative: only the services it lists can run. +// When there is no block (the embedded default), every service is enabled with +// data_dir /. DEVCLOUD_SERVICES and DEVCLOUD_DATA_DIR are applied +// here so both paths agree. +func (c *Config) Service(serviceID string) ServiceConfig { + if c.allowed != nil && !c.allowed[serviceID] { + return ServiceConfig{} + } + svc, listed := c.Services[serviceID] + if !listed { + if len(c.Services) > 0 { + return ServiceConfig{} + } + svc = ServiceConfig{Enabled: true} + } + if c.baseDir == "" && svc.DataDir != "" { + return svc + } + base := c.baseDir + if base == "" { + base = defaultDataDir + } + svc.DataDir = filepath.Join(base, serviceID) + return svc +} + +// Load reads and parses a YAML config file from the given path. It returns any +// configuration warnings alongside the config so the caller can log them +// through the operator-configured handler (see Logging). +func Load(path string) (*Config, []string, error) { data, err := os.ReadFile(path) if err != nil { - return nil, err + return nil, nil, err } return parse(data) } @@ -70,31 +103,33 @@ func Load(path string) (*Config, error) { // missing, it returns the embedded default configuration instead. Any other // read or parse error is returned as-is. This is the recommended entry point // for CLI usage so the server runs with zero setup. -func LoadOrDefault(fallbackPath string) (*Config, error) { +func LoadOrDefault(fallbackPath string) (*Config, []string, error) { if fallbackPath != "" { if _, err := os.Stat(fallbackPath); err == nil { return Load(fallbackPath) } else if !os.IsNotExist(err) { - return nil, err + return nil, nil, err } - slog.Info("config file not found, using embedded defaults", "tried", fallbackPath) } return parse(defaultConfigYAML) } -func parse(data []byte) (*Config, error) { +func parse(data []byte) (*Config, []string, error) { cfg := &Config{} if err := yaml.Unmarshal(data, cfg); err != nil { - return nil, err + return nil, nil, err } + var warnings []string + // Back-compat: 'dashboard' was renamed to 'admin'. Honour the old key for // one release so existing configs don't silently lose the admin API. An // explicit 'admin' block always wins; the deprecated key is used only when // no 'admin' block is present (so an explicit admin.enabled: false is not // overridden by a leftover dashboard.enabled: true). if cfg.Dashboard != nil { - slog.Warn("config: 'dashboard' key is deprecated and will be removed; rename it to 'admin'") + warnings = append(warnings, + "config: 'dashboard' key is deprecated and will be removed; rename it to 'admin'") if cfg.Admin == nil { cfg.Admin = cfg.Dashboard } @@ -108,8 +143,7 @@ func parse(data []byte) (*Config, error) { cfg.Server.Port = 4747 } - applyEnvOverrides(cfg) - return cfg, nil + return cfg, append(warnings, applyEnvOverrides(cfg)...), nil } var serviceTiers = map[string][]string{ @@ -130,10 +164,14 @@ var serviceTiers = map[string][]string{ }, } -func expandTiers(value string) map[string]bool { +// expandTiers resolves a DEVCLOUD_SERVICES value into the set of allowed +// service names, returning nil for "all" (no filtering). Tokens that look like +// a mistyped tier shortcut are reported as warnings. +func expandTiers(value string) (map[string]bool, []string) { if value == "all" { - return nil + return nil, nil } + var warnings []string allowed := make(map[string]bool) for _, token := range strings.Split(value, ",") { token = strings.TrimSpace(token) @@ -150,41 +188,25 @@ func expandTiers(value string) map[string]bool { // name. If it looks like it was *meant* to be a tier ("tierXXX"), // warn the operator so a typo surfaces in logs. if token != "all" && strings.HasPrefix(token, "tier") { - slog.Warn("DEVCLOUD_SERVICES: unknown tier shortcut; treating as literal service name", - "token", token) + warnings = append(warnings, fmt.Sprintf( + "DEVCLOUD_SERVICES: unknown tier shortcut %q; treating as a literal service name", token)) } allowed[token] = true } - return allowed + return allowed, warnings } -func applyEnvOverrides(cfg *Config) { +func applyEnvOverrides(cfg *Config) []string { if p := os.Getenv("DEVCLOUD_PORT"); p != "" { if v, err := strconv.Atoi(p); err == nil { cfg.Server.Port = v } } + var warnings []string if envServices := os.Getenv("DEVCLOUD_SERVICES"); envServices != "" { - allowed := expandTiers(envServices) - if allowed == nil { - return - } - for name, svc := range cfg.Services { - if !allowed[name] { - svc.Enabled = false - cfg.Services[name] = svc - } - } - } - - // DEVCLOUD_DATA_DIR overrides the base data directory for all services. - // Each service's data_dir is rewritten to / using - // the host's path separator. - if baseDir := os.Getenv("DEVCLOUD_DATA_DIR"); baseDir != "" { - for name, svc := range cfg.Services { - svc.DataDir = filepath.Join(baseDir, name) - cfg.Services[name] = svc - } + cfg.allowed, warnings = expandTiers(envServices) } + cfg.baseDir = os.Getenv("DEVCLOUD_DATA_DIR") + return warnings } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 880f7429..2bddebb2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -12,12 +12,13 @@ import ( ) func TestExpandTiers_All(t *testing.T) { - result := expandTiers("all") + result, warnings := expandTiers("all") assert.Nil(t, result, "all should return nil (no filtering)") + assert.Empty(t, warnings) } func TestExpandTiers_Tier1(t *testing.T) { - result := expandTiers("tier1") + result, _ := expandTiers("tier1") assert.Contains(t, result, "s3") assert.Contains(t, result, "sns") assert.Contains(t, result, "kms") @@ -26,13 +27,27 @@ func TestExpandTiers_Tier1(t *testing.T) { } func TestExpandTiers_Mixed(t *testing.T) { - result := expandTiers("tier1,cognito,rds") + result, _ := expandTiers("tier1,cognito,rds") assert.Contains(t, result, "s3") assert.Contains(t, result, "cognito") assert.Contains(t, result, "rds") } -func TestApplyEnvOverrides_TierBased(t *testing.T) { +// TestExpandTiers_UnknownToken_TreatedAsService documents the current +// behavior: unknown tokens are passed through as service names, allowing +// users to mix tier shortcuts with individual service names. A token that +// looks like a mistyped tier is reported as a warning. +func TestExpandTiers_UnknownToken_TreatedAsService(t *testing.T) { + result, warnings := expandTiers("tier1,kinesis,some-custom-service,tier9") + assert.Contains(t, result, "s3", "tier1 expanded") + assert.Contains(t, result, "kinesis", "explicit service included") + assert.Contains(t, result, "some-custom-service", + "unknown tokens pass through as service names (current contract)") + require.Len(t, warnings, 1, "only the tier-looking typo warns") + assert.Contains(t, warnings[0], "tier9") +} + +func TestService_EnvServiceFilter(t *testing.T) { cfg := &Config{ Services: map[string]ServiceConfig{ "s3": {Enabled: true}, @@ -41,8 +56,46 @@ func TestApplyEnvOverrides_TierBased(t *testing.T) { } t.Setenv("DEVCLOUD_SERVICES", "s3") applyEnvOverrides(cfg) - assert.True(t, cfg.Services["s3"].Enabled) - assert.False(t, cfg.Services["sqs"].Enabled) + assert.True(t, cfg.Service("s3").Enabled) + assert.False(t, cfg.Service("sqs").Enabled) +} + +// TestService_NoServicesBlock_EnablesEverything covers the zero-config +// contract: with no services block every service is enabled and lands under +// ./data/. +func TestService_NoServicesBlock_EnablesEverything(t *testing.T) { + cfg, _, err := parse([]byte("server:\n port: 4747\n")) + require.NoError(t, err) + + svc := cfg.Service("s3") + assert.True(t, svc.Enabled) + assert.Equal(t, filepath.Join("./data", "s3"), svc.DataDir) + assert.True(t, cfg.Service("some-new-service").Enabled, + "a service absent from the config still runs when there is no services block") +} + +// TestService_ExplicitBlockIsAuthoritative verifies that listing any service +// restricts startup to that list — otherwise a minimal config would silently +// start all 100+ services. +func TestService_ExplicitBlockIsAuthoritative(t *testing.T) { + cfg, _, err := parse([]byte("services:\n s3:\n enabled: true\n")) + require.NoError(t, err) + assert.True(t, cfg.Service("s3").Enabled) + assert.False(t, cfg.Service("sqs").Enabled, "unlisted service must not start") +} + +func TestService_DataDirOverride(t *testing.T) { + yaml := []byte("services:\n s3:\n enabled: true\n data_dir: ./custom/s3\n") + + cfg, _, err := parse(yaml) + require.NoError(t, err) + assert.Equal(t, "./custom/s3", cfg.Service("s3").DataDir) + + t.Setenv("DEVCLOUD_DATA_DIR", "/tmp/dc") + cfg, _, err = parse(yaml) + require.NoError(t, err) + assert.Equal(t, filepath.Join("/tmp/dc", "s3"), cfg.Service("s3").DataDir, + "DEVCLOUD_DATA_DIR overrides data_dir") } func TestLoadConfig_DefaultFile(t *testing.T) { @@ -55,8 +108,6 @@ services: s3: enabled: true data_dir: ./data/s3 -auth: - enabled: false admin: enabled: false logging: @@ -65,46 +116,26 @@ logging: `), 0644) require.NoError(t, err) - cfg, err := Load(cfgPath) + cfg, warnings, err := Load(cfgPath) require.NoError(t, err) + assert.Empty(t, warnings) assert.Equal(t, 4747, cfg.Server.Port) - assert.True(t, cfg.Services["s3"].Enabled) - assert.Equal(t, "./data/s3", cfg.Services["s3"].DataDir) - assert.False(t, cfg.Auth.Enabled) + assert.True(t, cfg.Service("s3").Enabled) + assert.Equal(t, "./data/s3", cfg.Service("s3").DataDir) + assert.False(t, cfg.Admin.Enabled) assert.Equal(t, "info", cfg.Logging.Level) } -func TestLoadConfig_EnvOverride(t *testing.T) { - dir := t.TempDir() - cfgPath := filepath.Join(dir, "devcloud.yaml") - err := os.WriteFile(cfgPath, []byte(` -server: - port: 4747 -services: - s3: - enabled: true - sqs: - enabled: true -`), 0644) - require.NoError(t, err) - - t.Setenv("DEVCLOUD_SERVICES", "s3") - cfg, err := Load(cfgPath) - require.NoError(t, err) - assert.True(t, cfg.Services["s3"].Enabled) - assert.False(t, cfg.Services["sqs"].Enabled) -} - // TestLoadOrDefault_FileMissing_UsesEmbedded verifies zero-config startup: // when the fallback path doesn't exist, LoadOrDefault returns the embedded -// default configuration (which has all services enabled and port 4747). +// default configuration (port 4747, every service enabled). func TestLoadOrDefault_FileMissing_UsesEmbedded(t *testing.T) { - cfg, err := LoadOrDefault(filepath.Join(t.TempDir(), "nonexistent.yaml")) + cfg, _, err := LoadOrDefault(filepath.Join(t.TempDir(), "nonexistent.yaml")) require.NoError(t, err) require.NotNil(t, cfg) assert.Equal(t, 4747, cfg.Server.Port, "embedded default port should be 4747") - assert.NotEmpty(t, cfg.Services, "embedded default should have services") - assert.True(t, cfg.Services["s3"].Enabled, "s3 should be enabled in embedded default") + assert.Empty(t, cfg.Services, "embedded default carries no services block") + assert.True(t, cfg.Service("s3").Enabled, "s3 should be enabled in embedded default") } // TestLoadOrDefault_FileExists_UsesFile verifies that when the fallback path @@ -121,69 +152,62 @@ services: data_dir: ./custom/s3 `), 0644)) - cfg, err := LoadOrDefault(cfgPath) + cfg, _, err := LoadOrDefault(cfgPath) require.NoError(t, err) assert.Equal(t, 5858, cfg.Server.Port, "should use file's port, not embedded default") - assert.Equal(t, "./custom/s3", cfg.Services["s3"].DataDir) + assert.Equal(t, "./custom/s3", cfg.Service("s3").DataDir) } // TestLoadOrDefault_EmptyPath_UsesEmbedded verifies that an empty fallback // path skips the file check entirely and uses embedded defaults. func TestLoadOrDefault_EmptyPath_UsesEmbedded(t *testing.T) { - cfg, err := LoadOrDefault("") + cfg, _, err := LoadOrDefault("") require.NoError(t, err) assert.Equal(t, 4747, cfg.Server.Port) - assert.NotEmpty(t, cfg.Services) + assert.True(t, cfg.Service("dynamodb").Enabled) } // TestLoad_ExplicitPathMissing_ReturnsError verifies that Load (strict) // surfaces an error when the explicit path does not exist — unlike // LoadOrDefault, which falls back to embedded defaults. func TestLoad_ExplicitPathMissing_ReturnsError(t *testing.T) { - _, err := Load(filepath.Join(t.TempDir(), "nope.yaml")) + _, _, err := Load(filepath.Join(t.TempDir(), "nope.yaml")) require.Error(t, err, "Load should error on missing file; use LoadOrDefault for graceful fallback") } -// TestParse_EmptyData_FillsDefaults verifies that parsing an empty YAML -// payload yields a Config with at least the default server port populated, -// so downstream code sees a usable config rather than a zero-value one. -func TestParse_EmptyData_FillsDefaults(t *testing.T) { - cfg, err := parse([]byte("")) - require.NoError(t, err) - assert.Equal(t, 4747, cfg.Server.Port, "empty data should still yield default port 4747") -} - // TestParse_DeprecatedDashboardKey verifies the legacy 'dashboard' config key // still enables the admin API after the rename (with a deprecation warning), // so existing deployments don't silently lose it. func TestParse_DeprecatedDashboardKey(t *testing.T) { - cfg, err := parse([]byte("dashboard:\n enabled: true\n")) + cfg, warnings, err := parse([]byte("dashboard:\n enabled: true\n")) require.NoError(t, err) assert.True(t, cfg.Admin.Enabled, "deprecated dashboard.enabled=true should enable admin") assert.Nil(t, cfg.Dashboard, "deprecated key should be cleared after folding into admin") + require.Len(t, warnings, 1) + assert.Contains(t, warnings[0], "deprecated") } // TestParse_AdminKeyWinsOverDeprecated verifies an explicit 'admin' block takes // precedence over the deprecated 'dashboard' key when both are present. func TestParse_AdminKeyWinsOverDeprecated(t *testing.T) { - cfg, err := parse([]byte("admin:\n enabled: true\ndashboard:\n enabled: false\n")) + cfg, _, err := parse([]byte("admin:\n enabled: true\ndashboard:\n enabled: false\n")) require.NoError(t, err) assert.True(t, cfg.Admin.Enabled, "explicit admin.enabled=true should win") // The reverse precedence must also hold: an explicit admin.enabled=false // must not be re-enabled by a leftover deprecated dashboard.enabled=true. - cfg, err = parse([]byte("admin:\n enabled: false\ndashboard:\n enabled: true\n")) + cfg, _, err = parse([]byte("admin:\n enabled: false\ndashboard:\n enabled: true\n")) require.NoError(t, err) assert.False(t, cfg.Admin.Enabled, "explicit admin.enabled=false should win over deprecated dashboard.enabled=true") } -// TestExpandTiers_UnknownToken_TreatedAsService documents the current -// behavior: unknown tokens are passed through as service names, allowing -// users to mix tier shortcuts with individual service names. -func TestExpandTiers_UnknownToken_TreatedAsService(t *testing.T) { - result := expandTiers("tier1,kinesis,some-custom-service") - assert.Contains(t, result, "s3", "tier1 expanded") - assert.Contains(t, result, "kinesis", "explicit service included") - assert.Contains(t, result, "some-custom-service", - "unknown tokens pass through as service names (current contract)") +// TestParse_EmptyData_FillsDefaults verifies that parsing an empty YAML +// payload yields a Config with at least the default server port populated, +// so downstream code sees a usable config rather than a zero-value one. +func TestParse_EmptyData_FillsDefaults(t *testing.T) { + cfg, _, err := parse([]byte("")) + require.NoError(t, err) + assert.Equal(t, 4747, cfg.Server.Port, "empty data should still yield default port 4747") + require.NotNil(t, cfg.Admin) + assert.False(t, cfg.Admin.Enabled) } diff --git a/internal/config/default.yaml b/internal/config/default.yaml index 2caf3c72..15fc1b1d 100644 --- a/internal/config/default.yaml +++ b/internal/config/default.yaml @@ -1,322 +1,13 @@ +# Zero-config defaults. +# +# There is deliberately no `services:` block: every registered service starts +# with data_dir ./data/. Add a services block to run a subset — it is +# authoritative, so only the services it lists start. Use DEVCLOUD_SERVICES for +# a quick subset without a file, and DEVCLOUD_DATA_DIR to relocate the data +# directories. server: port: 4747 -services: - s3: - enabled: true - data_dir: ./data/s3 - sqs: - enabled: true - dynamodb: - enabled: true - data_dir: ./data/dynamodb - iam: - enabled: true - data_dir: ./data/iam - sts: - enabled: true - data_dir: ./data/sts - lambda: - enabled: true - data_dir: ./data/lambda - sns: - enabled: true - data_dir: ./data/sns - kms: - enabled: true - data_dir: ./data/kms - secretsmanager: - enabled: true - data_dir: ./data/secretsmanager - ssm: - enabled: true - data_dir: ./data/ssm - cloudwatchlogs: - enabled: true - data_dir: ./data/cloudwatchlogs - cloudwatch: - enabled: true - data_dir: ./data/cloudwatch - eventbridge: - enabled: true - data_dir: ./data/eventbridge - ec2: - enabled: true - data_dir: ./data/ec2 - ecs: - enabled: true - data_dir: ./data/ecs - ecr: - enabled: true - data_dir: ./data/ecr - route53: - enabled: true - data_dir: ./data/route53 - acm: - enabled: true - data_dir: ./data/acm - rds: - enabled: true - data_dir: ./data/rds - cloudformation: - enabled: true - data_dir: ./data/cloudformation - elasticache: - enabled: true - data_dir: ./data/elasticache - elasticloadbalancingv2: - enabled: true - data_dir: ./data/elasticloadbalancingv2 - redshift: - enabled: true - data_dir: ./data/redshift - ses: - enabled: true - data_dir: ./data/ses - autoscaling: - enabled: true - data_dir: ./data/autoscaling - docdb: - enabled: true - data_dir: ./data/docdb - neptune: - enabled: true - data_dir: ./data/neptune - elasticbeanstalk: - enabled: true - data_dir: ./data/elasticbeanstalk - cloudsearch: - enabled: true - data_dir: ./data/cloudsearch - apigatewayv2: - enabled: true - data_dir: ./data/apigatewayv2 - opensearch: - enabled: true - data_dir: ./data/opensearch - waf: - enabled: true - data_dir: ./data/waf - backup: - enabled: true - data_dir: ./data/backup - glue: - enabled: true - data_dir: ./data/glue - sagemaker: - enabled: true - data_dir: ./data/sagemaker - iot: - enabled: true - data_dir: ./data/iot - route53resolver: - enabled: true - data_dir: ./data/route53resolver - sesv2: - enabled: true - data_dir: ./data/sesv2 - pinpoint: - enabled: true - data_dir: ./data/pinpoint - acmpca: - enabled: true - data_dir: ./data/acmpca - amplify: - enabled: true - data_dir: ./data/amplify - appconfig: - enabled: true - data_dir: ./data/appconfig - applicationautoscaling: - enabled: true - data_dir: ./data/applicationautoscaling - appsync: - enabled: true - data_dir: ./data/appsync - athena: - enabled: true - data_dir: ./data/athena - batch: - enabled: true - data_dir: ./data/batch - bedrock: - enabled: true - data_dir: ./data/bedrock - cloudfront: - enabled: true - data_dir: ./data/cloudfront - cloudtrail: - enabled: true - data_dir: ./data/cloudtrail - codeartifact: - enabled: true - data_dir: ./data/codeartifact - codebuild: - enabled: true - data_dir: ./data/codebuild - codecommit: - enabled: true - data_dir: ./data/codecommit - codedeploy: - enabled: true - data_dir: ./data/codedeploy - codepipeline: - enabled: true - data_dir: ./data/codepipeline - cognitoidentity: - enabled: true - data_dir: ./data/cognitoidentity - cognitoidentityprovider: - enabled: true - data_dir: ./data/cognitoidentityprovider - configservice: - enabled: true - data_dir: ./data/configservice - costexplorer: - enabled: true - data_dir: ./data/costexplorer - dynamodbstreams: - enabled: true - data_dir: ./data/dynamodbstreams - ebs: - enabled: true - data_dir: ./data/ebs - efs: - enabled: true - data_dir: ./data/efs - eks: - enabled: true - data_dir: ./data/eks - elasticsearchservice: - enabled: true - data_dir: ./data/elasticsearchservice - emr: - enabled: true - data_dir: ./data/emr - firehose: - enabled: true - data_dir: ./data/firehose - fis: - enabled: true - data_dir: ./data/fis - glacier: - enabled: true - data_dir: ./data/glacier - iotdataplane: - enabled: true - data_dir: ./data/iotdataplane - iotwireless: - enabled: true - data_dir: ./data/iotwireless - kafka: - enabled: true - data_dir: ./data/kafka - kinesis: - enabled: true - data_dir: ./data/kinesis - kinesisanalyticsv2: - enabled: true - data_dir: ./data/kinesisanalyticsv2 - lakeformation: - enabled: true - data_dir: ./data/lakeformation - managedblockchain: - enabled: true - data_dir: ./data/managedblockchain - memorydb: - enabled: true - data_dir: ./data/memorydb - mq: - enabled: true - data_dir: ./data/mq - mwaa: - enabled: true - data_dir: ./data/mwaa - organizations: - enabled: true - data_dir: ./data/organizations - ram: - enabled: true - data_dir: ./data/ram - resourcegroups: - enabled: true - data_dir: ./data/resourcegroups - resourcegroupstaggingapi: - enabled: true - data_dir: ./data/resourcegroupstaggingapi - servicediscovery: - enabled: true - data_dir: ./data/servicediscovery - sfn: - enabled: true - data_dir: ./data/sfn - shield: - enabled: true - data_dir: ./data/shield - ssoadmin: - enabled: true - data_dir: ./data/ssoadmin - support: - enabled: true - data_dir: ./data/support - swf: - enabled: true - data_dir: ./data/swf - textract: - enabled: true - data_dir: ./data/textract - timestreamwrite: - enabled: true - data_dir: ./data/timestreamwrite - transcribe: - enabled: true - data_dir: ./data/transcribe - transfer: - enabled: true - data_dir: ./data/transfer - wafv2: - enabled: true - data_dir: ./data/wafv2 - xray: - enabled: true - data_dir: ./data/xray - account: - enabled: true - data_dir: ./data/account - cloudcontrol: - enabled: true - data_dir: ./data/cloudcontrol - pipes: - enabled: true - data_dir: ./data/pipes - mediaconvert: - enabled: true - data_dir: ./data/mediaconvert - s3tables: - enabled: true - data_dir: ./data/s3tables - scheduler: - enabled: true - data_dir: ./data/scheduler - identitystore: - enabled: true - data_dir: ./data/identitystore - serverlessrepo: - enabled: true - data_dir: ./data/serverlessrepo - codeconnections: - enabled: true - data_dir: ./data/codeconnections - dms: - enabled: true - data_dir: ./data/dms - verifiedpermissions: - enabled: true - data_dir: ./data/verifiedpermissions - -auth: - enabled: false - admin: enabled: false diff --git a/internal/eventbus/eventbus.go b/internal/eventbus/eventbus.go deleted file mode 100644 index 9baa5723..00000000 --- a/internal/eventbus/eventbus.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package eventbus - -import ( - "context" - "log/slog" - "sync" - "time" -) - -type Event struct { - Source string - Type string - Detail map[string]any - Timestamp time.Time -} - -type EventHandler func(Event) - -type EventBus interface { - Publish(ctx context.Context, event Event) error - Subscribe(eventType string, handler EventHandler) (unsubscribe func()) -} - -type subscriber struct { - eventType string - handler EventHandler -} - -type InMemoryEventBus struct { - mu sync.RWMutex - subscribers map[uint64]*subscriber - nextID uint64 -} - -func New() *InMemoryEventBus { - return &InMemoryEventBus{ - subscribers: make(map[uint64]*subscriber), - } -} - -func (b *InMemoryEventBus) Publish(_ context.Context, event Event) error { - if event.Timestamp.IsZero() { - event.Timestamp = time.Now() - } - b.mu.RLock() - subs := make([]*subscriber, 0, len(b.subscribers)) - for _, s := range b.subscribers { - subs = append(subs, s) - } - b.mu.RUnlock() - - for _, s := range subs { - if s.eventType == "*" || s.eventType == event.Type { - go func(h EventHandler, e Event) { - defer func() { - if r := recover(); r != nil { - slog.Error("event handler panic", "event", e.Type, "panic", r) - } - }() - h(e) - }(s.handler, event) - } - } - return nil -} - -func (b *InMemoryEventBus) Subscribe(eventType string, handler EventHandler) func() { - b.mu.Lock() - id := b.nextID - b.nextID++ - b.subscribers[id] = &subscriber{eventType: eventType, handler: handler} - b.mu.Unlock() - - return func() { - b.mu.Lock() - delete(b.subscribers, id) - b.mu.Unlock() - } -} diff --git a/internal/eventbus/eventbus_test.go b/internal/eventbus/eventbus_test.go deleted file mode 100644 index 62a4b6ed..00000000 --- a/internal/eventbus/eventbus_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package eventbus - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPublishAndSubscribe(t *testing.T) { - bus := New() - var received []Event - var mu sync.Mutex - - unsub := bus.Subscribe("s3:ObjectCreated", func(e Event) { - mu.Lock() - received = append(received, e) - mu.Unlock() - }) - defer unsub() - - require.NoError(t, bus.Publish(context.Background(), Event{ - Source: "s3", - Type: "s3:ObjectCreated", - Detail: map[string]any{"bucket": "test", "key": "hello.txt"}, - })) - - time.Sleep(50 * time.Millisecond) - mu.Lock() - defer mu.Unlock() - require.Len(t, received, 1) - assert.Equal(t, "s3", received[0].Source) - assert.Equal(t, "test", received[0].Detail["bucket"]) -} - -func TestSubscribeWildcard(t *testing.T) { - bus := New() - var received []Event - var mu sync.Mutex - - unsub := bus.Subscribe("*", func(e Event) { - mu.Lock() - received = append(received, e) - mu.Unlock() - }) - defer unsub() - - require.NoError(t, bus.Publish(context.Background(), Event{Source: "s3", Type: "s3:ObjectCreated"})) - require.NoError(t, bus.Publish(context.Background(), Event{Source: "sqs", Type: "sqs:MessageSent"})) - - time.Sleep(50 * time.Millisecond) - mu.Lock() - defer mu.Unlock() - assert.Len(t, received, 2) -} - -func TestUnsubscribe(t *testing.T) { - bus := New() - callCount := 0 - var mu sync.Mutex - - unsub := bus.Subscribe("test", func(e Event) { - mu.Lock() - callCount++ - mu.Unlock() - }) - - require.NoError(t, bus.Publish(context.Background(), Event{Type: "test"})) - time.Sleep(50 * time.Millisecond) - - unsub() - - require.NoError(t, bus.Publish(context.Background(), Event{Type: "test"})) - time.Sleep(50 * time.Millisecond) - - mu.Lock() - defer mu.Unlock() - assert.Equal(t, 1, callCount) -} From 6f962e82fb1f993ab5d3d26bebf6922c9cf2bbb4 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Thu, 30 Jul 2026 08:00:18 +0900 Subject: [PATCH 3/8] refactor: shrink protocol detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeServiceID went 248 lines to 133. Of its 117 cases, 79 arms were identity mappings (case "s3": return "s3") — but they were not simply redundant: they also lowercased their input, which `default: return svc` did not. The default now lowercases, which makes the identity arms genuinely dead and is strictly better for unmatched names, since every registry key is lowercase. The rewrite was verified by asserting that 385 labels (every case arm, every return value, their uppercase variants, plus unmatched samples) map identically before and after. One dead arm went too: "simpleWorkflowService" could never match a switch on strings.ToLower. serviceFromQueryRequest drops the Action-name whitelist. It only ran for a request carrying neither a SigV4 credential scope nor an iam/sts/sqs host prefix — and every AWS SDK, the CLI, and Terraform sign their requests. With the whitelist gone the QueueUrl probe is also redundant, since it returned the same value as the fallback, so the body argument and the net/url import go with it. Delete gateway/auth.go: ExtractAccountID read the Authorization header into _, returned a constant, and had no callers. --- internal/gateway/auth.go | 23 ---- internal/gateway/auth_test.go | 25 ---- internal/gateway/protocol.go | 233 +++++----------------------------- 3 files changed, 34 insertions(+), 247 deletions(-) delete mode 100644 internal/gateway/auth.go delete mode 100644 internal/gateway/auth_test.go diff --git a/internal/gateway/auth.go b/internal/gateway/auth.go deleted file mode 100644 index 31875cbe..00000000 --- a/internal/gateway/auth.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package gateway - -import ( - "net/http" - - "github.com/skyoo2003/devcloud/internal/plugin" -) - -// defaultAccountID is the account ID used for all requests until real -// account-to-credential mapping is implemented. -const defaultAccountID = plugin.DefaultAccountID - -// ExtractAccountID attempts to derive an AWS account ID from the incoming -// request (e.g. by inspecting the Authorization header). Real account mapping -// is deferred; the function always returns the default account ID for now. -func ExtractAccountID(r *http.Request) string { - // The Authorization header is available for future implementation of - // access-key → account-ID resolution. - _ = r.Header.Get("Authorization") - return defaultAccountID -} diff --git a/internal/gateway/auth_test.go b/internal/gateway/auth_test.go deleted file mode 100644 index 48c256e5..00000000 --- a/internal/gateway/auth_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package gateway - -import ( - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestExtractAccountID(t *testing.T) { - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Authorization", - "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, "+ - "SignedHeaders=host;range;x-amz-date, Signature=abc123") - accountID := ExtractAccountID(req) - assert.Equal(t, "000000000000", accountID) -} - -func TestExtractAccountID_NoAuth(t *testing.T) { - req := httptest.NewRequest("GET", "/", nil) - accountID := ExtractAccountID(req) - assert.Equal(t, "000000000000", accountID) -} diff --git a/internal/gateway/protocol.go b/internal/gateway/protocol.go index 593443d5..60e1e32e 100644 --- a/internal/gateway/protocol.go +++ b/internal/gateway/protocol.go @@ -6,7 +6,6 @@ import ( "bytes" "io" "net/http" - "net/url" "strings" ) @@ -35,8 +34,7 @@ func DetectProtocol(r *http.Request) (protocol string, serviceID string) { r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) if strings.Contains(string(bodyBytes), "Action=") { - service := serviceFromQueryRequest(r, string(bodyBytes)) - return "query", service + return "query", serviceFromQueryRequest(r) } } } @@ -134,24 +132,12 @@ func serviceFromSigV4(r *http.Request) string { // normalizeServiceID maps SigV4 signing names, X-Amz-Target prefixes, and // other AWS identifiers to DevCloud internal service IDs. func normalizeServiceID(svc string) string { - switch strings.ToLower(svc) { - // Core services - case "s3": - return "s3" - case "sqs", "amazonsqs": + svc = strings.ToLower(svc) + switch svc { + case "amazonsqs": return "sqs" - case "dynamodb", "dynamodb_20120810": + case "dynamodb_20120810": return "dynamodb" - case "iam": - return "iam" - case "sts": - return "sts" - case "lambda": - return "lambda" - case "sns": - return "sns" - - // JSON-protocol services (matched by X-Amz-Target prefix, lowercased) case "amazonssm": return "ssm" case "trentservice": @@ -166,8 +152,6 @@ func normalizeServiceID(svc string) string { return "ecs" case "amazonec2containerregistry", "amazonec2containerregistry_v20150921": return "ecr" - case "secretsmanager": - return "secretsmanager" case "certificatemanager": return "acm" case "awswaf", "awswaf_20150824": @@ -176,10 +160,6 @@ func normalizeServiceID(svc string) string { return "wafv2" case "awsglue": return "glue" - case "sagemaker": - return "sagemaker" - case "route53resolver": - return "route53resolver" case "codepipeline_20150709": return "codepipeline" case "codebuild_20161006": @@ -192,9 +172,9 @@ func normalizeServiceID(svc string) string { return "codeartifact" case "amazonkinesis": return "kinesis" - case "kinesisanalytics", "kinesisanalytics_v2", "kinesisanalyticsv2": + case "kinesisanalytics", "kinesisanalytics_v2": return "kinesisanalyticsv2" - case "firehose", "firehose_20150804": + case "firehose_20150804": return "firehose" case "amazonathena": return "athena" @@ -206,7 +186,7 @@ func normalizeServiceID(svc string) string { return "mwaa" case "awssfn", "awsstepfunctions": return "sfn" - case "swf", "simpleWorkflowService", "simpleworkflowservice": + case "simpleWorkflowService", "simpleworkflowservice": return "swf" case "swbexternalservice": return "ssoadmin" @@ -222,56 +202,34 @@ func normalizeServiceID(svc string) string { return "organizations" case "awsshield", "awsshield_20160616": return "shield" - case "ssoadmin", "sso": + case "sso": return "ssoadmin" case "awssupport", "awssupport_20130415": return "support" - case "awsfaultinjectionservice", "fis": + case "awsfaultinjectionservice": return "fis" - case "xray", "awsxray": + case "awsxray": return "xray" - case "timestreamwrite", "timestream_20181101", "timestream": + case "timestream_20181101", "timestream": return "timestreamwrite" - case "transcribe": - return "transcribe" - case "textract": - return "textract" - case "bedrock": - return "bedrock" - case "costexplorer", "awscostexplorer", "awsinsightsindexservice": + case "awscostexplorer", "awsinsightsindexservice": return "costexplorer" - case "batch", "awsbatch", "awsbatch_v20160810": + case "awsbatch", "awsbatch_v20160810": return "batch" - case "kafka", "msk": + case "msk": return "kafka" - case "lakeformation": - return "lakeformation" case "amazondmsv20160101": return "dms" - case "configservice", "config", "starlingdoveservice": + case "config", "starlingdoveservice": return "configservice" - case "applicationautoscaling", "application-autoscaling", "anyupfront", "anyscalefrontendservice": + case "application-autoscaling", "anyupfront", "anyscalefrontendservice": return "applicationautoscaling" - case "appconfig": - return "appconfig" - case "awsresourcegroups", "resourcegroups", "resource-groups": + case "awsresourcegroups", "resource-groups": return "resourcegroups" - case "resourcegroupstaggingapi", "resourcegroupstagging": + case "resourcegroupstagging": return "resourcegroupstaggingapi" - case "ram": - return "ram" - case "cloudcontrolapi", "cloudapiservice", "cloudcontrol": + case "cloudcontrolapi", "cloudapiservice": return "cloudcontrol" - case "pipes": - return "pipes" - case "account": - return "account" - - // SigV4 signing names for REST/Query services - case "ec2": - return "ec2" - case "route53": - return "route53" case "elasticloadbalancing": return "elasticloadbalancingv2" case "es": @@ -282,169 +240,46 @@ func normalizeServiceID(svc string) string { return "apigatewayv2" case "mobiletargeting": return "pinpoint" - case "backup": - return "backup" - case "iot": - return "iot" case "data.iot", "iotdata", "iot-data": return "iotdataplane" - case "iotwireless": - return "iotwireless" - case "amplify": - return "amplify" - case "appsync": - return "appsync" - case "cloudfront": - return "cloudfront" - case "acm-pca", "acmpca", "acmprivateca": + case "acm-pca", "acmprivateca": return "acmpca" - case "servicediscovery", "route53autonaming": + case "route53autonaming": return "servicediscovery" - case "eks": - return "eks" - case "efs", "elasticfilesystem": + case "elasticfilesystem": return "efs" - case "ebs": - return "ebs" - case "glacier": - return "glacier" - case "managedblockchain": - return "managedblockchain" - case "mediaconvert": - return "mediaconvert" - case "transfer", "transferservice": + case "transferservice": return "transfer" - case "codecommit": - return "codecommit" - case "codedeploy": - return "codedeploy" - case "codebuild": - return "codebuild" - case "codepipeline": - return "codepipeline" - case "codeartifact": - return "codeartifact" - case "cloudtrail", "cloudtrail_20131101": + case "cloudtrail_20131101": return "cloudtrail" - case "opensearch", "opensearchservice": + case "opensearchservice": return "opensearch" - case "s3tables": - return "s3tables" - case "identitystore", "awsidentitystore", "swbexternaluserservice": + case "awsidentitystore", "swbexternaluserservice": return "identitystore" - case "serverlessrepo", "serverlessapplicationrepository": + case "serverlessapplicationrepository": return "serverlessrepo" - case "scheduler": - return "scheduler" - - // Query-protocol services (signing name = service name) - case "rds": - return "rds" - case "cloudformation": - return "cloudformation" - case "elasticache": - return "elasticache" - case "redshift": - return "redshift" - case "ses": - return "ses" - case "autoscaling": - return "autoscaling" - case "elasticbeanstalk": - return "elasticbeanstalk" - case "cloudsearch": - return "cloudsearch" - case "ssm": - return "ssm" - case "kms": - return "kms" - case "ecs": - return "ecs" - case "ecr": - return "ecr" case "events": return "eventbridge" - case "acm": - return "acm" - case "cloudwatchlogs": - return "cloudwatchlogs" - case "cloudwatch": - return "cloudwatch" - case "eventbridge": - return "eventbridge" - case "glue": - return "glue" - case "waf": - return "waf" - default: return svc } } // serviceFromQueryRequest determines the service for a Query-protocol request -// by examining the SigV4 credential scope, Host header prefix, and the Action parameter. -func serviceFromQueryRequest(r *http.Request, body string) string { - // Most reliable: extract service from SigV4 Authorization header +// from the SigV4 credential scope, falling back to the Host header prefix. +// Every AWS SDK, CLI, and Terraform request is signed, so the credential scope +// carries the signing name; sqs is the default for the unsigned, unprefixed +// case because it is the only Query service SDKs address by bare endpoint. +func serviceFromQueryRequest(r *http.Request) string { if svc := serviceFromSigV4(r); svc != "" { return svc } - host := r.Host - - // Check the host prefix. - hostPrefix := strings.ToLower(strings.SplitN(host, ".", 2)[0]) - switch hostPrefix { + switch strings.ToLower(strings.SplitN(r.Host, ".", 2)[0]) { case "iam": return "iam" case "sts": return "sts" - case "sqs": - return "sqs" } - - // Fall back to Action name inspection. - values, err := url.ParseQuery(body) - if err == nil { - action := values.Get("Action") - switch { - case strings.HasPrefix(action, "CreateUser"), - strings.HasPrefix(action, "DeleteUser"), - strings.HasPrefix(action, "ListUsers"), - strings.HasPrefix(action, "AttachRole"), - strings.HasPrefix(action, "CreateRole"), - strings.HasPrefix(action, "CreatePolicy"), - strings.HasPrefix(action, "ListAttachedRole"), - strings.HasPrefix(action, "CreateAccessKey"), - action == "CreateGroup", - action == "DeleteGroup": - return "iam" - case action == "GetCallerIdentity", - action == "AssumeRole", - action == "GetSessionToken": - return "sts" - case action == "SendMessage", - action == "ReceiveMessage", - action == "DeleteMessage", - action == "CreateQueue", - action == "GetQueueUrl", - action == "ListQueues", - action == "DeleteQueue", - action == "GetQueueAttributes", - action == "SetQueueAttributes", - action == "PurgeQueue", - action == "ChangeMessageVisibility", - action == "TagQueue", - action == "UntagQueue": - return "sqs" - } - - // Detect SQS by presence of QueueUrl parameter. - if values.Get("QueueUrl") != "" { - return "sqs" - } - } - - // Default Query service. return "sqs" } From 8f433a1ab5fd37fb72a8613c89862d4c87fd0842 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Thu, 30 Jul 2026 08:00:41 +0900 Subject: [PATCH 4/8] fix: the weekly Smithy model sync could never detect an update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit download-smithy-models.sh skipped any model already on disk, and every model is committed, so the weekly job re-ran codegen over unchanged inputs and found nothing every week. Its hand-maintained 40-line SERVICES list had also drifted from upstream naming, so 14 entries (secretsmanager, logs, monitoring, events, route53, apigateway, autoscaling, dms, ...) 404'd on every run. Verified against upstream: the committed sqs and kms models are stale. The list is now derived from the models present, so it cannot drift; name a service explicitly to add a new one. The workflow passes --refresh to re-download, and downloads land through a temp file, so a failed fetch can no longer delete or truncate a committed model — the previous code did rm -f on the destination. The change check moves to git status --porcelain because git diff --quiet cannot see a newly generated (untracked) package. The models stay committed on purpose, and the script now says why: BASE_URL tracks aws-sdk-go-v2 main, so they are the pin that makes `make codegen` reproducible and offline. Only the weekly job refreshes them, which is what turns an upstream API change into a reviewable model diff. codegen now gofmts what it writes, so a fresh `make codegen` is byte-identical to the committed tree instead of showing a whole-tree reformat. That made one thing visible: codegen was resurrecting internal/generated/sts, 563 lines deleted in #91 and #96 because STS is hand-written in internal/services/iam, as untracked files the sync's diff check could not see. STS is now skipped; it is Query-protocol, so it contributes nothing to the JSON-only CRUD registry. --- .github/workflows/smithy-sync.yml | 11 +++- cmd/codegen/main.go | 15 ++++- internal/codegen/generator.go | 78 +++++++++++++----------- scripts/download-smithy-models.sh | 98 +++++++++++++++++++------------ 4 files changed, 125 insertions(+), 77 deletions(-) diff --git a/.github/workflows/smithy-sync.yml b/.github/workflows/smithy-sync.yml index b2dc733d..4de28b65 100644 --- a/.github/workflows/smithy-sync.yml +++ b/.github/workflows/smithy-sync.yml @@ -24,7 +24,9 @@ jobs: run: sudo apt-get install -y libsqlite3-dev - name: Download latest Smithy models - run: bash scripts/download-smithy-models.sh + # --refresh re-downloads the committed models; without it every model + # already in the tree is skipped and this job can never find a change. + run: bash scripts/download-smithy-models.sh --refresh - name: Run code generation run: | @@ -39,8 +41,11 @@ jobs: - name: Check for changes id: changes + # --porcelain rather than `git diff --quiet` so a newly generated + # package (an untracked directory) counts as a change instead of being + # silently dropped on the runner. run: | - if git diff --quiet; then + if [ -z "$(git status --porcelain)" ]; then echo "changed=false" >> $GITHUB_OUTPUT else echo "changed=true" >> $GITHUB_OUTPUT @@ -48,7 +53,7 @@ jobs: - name: Run tests if: steps.changes.outputs.changed == 'true' - run: CGO_ENABLED=1 go test ./internal/... -v + run: CGO_ENABLED=1 go test ./... -v - name: Create Pull Request if: steps.changes.outputs.changed == 'true' diff --git a/cmd/codegen/main.go b/cmd/codegen/main.go index 2c560f60..b2aa4d89 100644 --- a/cmd/codegen/main.go +++ b/cmd/codegen/main.go @@ -13,6 +13,14 @@ import ( "github.com/skyoo2003/devcloud/internal/codegen" ) +// handWritten services get no generated package: their provider is implemented +// by hand elsewhere (sts lives in internal/services/iam alongside its store). +// Their generated stubs were deleted in #91 and #96; without this skip every +// `make codegen` run resurrects them as untracked files that the sync +// workflow's diff check cannot see. sts is Query-protocol, so it contributes +// nothing to the JSON-only CRUD registry either. +var handWritten = map[string]bool{"sts": true} + func main() { modelsDir := flag.String("models", "./smithy-models", "Directory containing Smithy JSON model files") outputDir := flag.String("output", "./internal/generated", "Output directory for generated code") @@ -59,6 +67,11 @@ func main() { continue } + if handWritten[model.ServiceID] { + fmt.Printf("Skipping %s (%s): hand-written provider\n", model.ServiceName, model.ServiceID) + continue + } + fmt.Printf("Generating %s (%s)...\n", model.ServiceName, model.ServiceID) if err := gen.GenerateAll(model, *outputDir, *scaffoldDir); err != nil { fmt.Fprintf(os.Stderr, "Error generating %s: %v\n", model.ServiceID, err) @@ -86,7 +99,7 @@ func main() { fmt.Fprintf(os.Stderr, "Error creating crudregistry dir: %v\n", err) os.Exit(1) } - if err := os.WriteFile(filepath.Join(regDir, "registry_gen.go"), []byte(content), 0644); err != nil { + if err := codegen.WriteGo(filepath.Join(regDir, "registry_gen.go"), content); err != nil { fmt.Fprintf(os.Stderr, "Error writing CRUD registry: %v\n", err) os.Exit(1) } diff --git a/internal/codegen/generator.go b/internal/codegen/generator.go index 711e6b70..66f57305 100644 --- a/internal/codegen/generator.go +++ b/internal/codegen/generator.go @@ -5,10 +5,22 @@ package codegen import ( "fmt" + "go/format" "os" "path/filepath" ) +// WriteGo gofmts content and writes it to path. Templates emit close-enough Go; +// formatting here keeps a fresh `make codegen` byte-identical to what is +// committed, so a regeneration diff shows real model changes and nothing else. +func WriteGo(path, content string) error { + formatted, err := format.Source([]byte(content)) + if err != nil { + return fmt.Errorf("gofmt %s: %w", path, err) + } + return os.WriteFile(path, formatted, 0644) +} + func (g *Generator) GenerateAll(model *SmithyModel, outputDir string, scaffoldDir string) error { pkgName := model.ServiceID serviceDir := filepath.Join(outputDir, pkgName) @@ -16,49 +28,47 @@ func (g *Generator) GenerateAll(model *SmithyModel, outputDir string, scaffoldDi return fmt.Errorf("mkdir %s: %w", serviceDir, err) } - generators := map[string]func(string, *SmithyModel) (string, error){ - "types.go": g.GenerateTypes, - "base_provider.go": g.GenerateBaseProvider, - "router.go": g.GenerateRouter, - "errors.go": g.GenerateErrors, + generators := []struct { + fileName string + generate func(string, *SmithyModel) (string, error) + }{ + {"types.go", g.GenerateTypes}, + {"base_provider.go", g.GenerateBaseProvider}, + {"router.go", g.GenerateRouter}, + {"errors.go", g.GenerateErrors}, } - for fileName, genFunc := range generators { - content, err := genFunc(pkgName, model) + for _, gen := range generators { + content, err := gen.generate(pkgName, model) if err != nil { - return fmt.Errorf("generate %s: %w", fileName, err) + return fmt.Errorf("generate %s: %w", gen.fileName, err) } - outPath := filepath.Join(serviceDir, fileName) - if err := os.WriteFile(outPath, []byte(content), 0644); err != nil { + outPath := filepath.Join(serviceDir, gen.fileName) + if err := WriteGo(outPath, content); err != nil { return fmt.Errorf("write %s: %w", outPath, err) } } - // Scaffold files — only written if they don't already exist. - if scaffoldDir != "" { - scaffoldServiceDir := filepath.Join(scaffoldDir, pkgName) - if err := os.MkdirAll(scaffoldServiceDir, 0755); err != nil { - return fmt.Errorf("mkdir scaffold %s: %w", scaffoldServiceDir, err) - } - - scaffolds := map[string]func(string, *SmithyModel) (string, error){ - "provider.go": g.GenerateScaffold, - } - - for fileName, genFunc := range scaffolds { - outPath := filepath.Join(scaffoldServiceDir, fileName) - if _, err := os.Stat(outPath); err == nil { - continue // file exists, skip - } - content, err := genFunc(pkgName, model) - if err != nil { - return fmt.Errorf("generate scaffold %s: %w", fileName, err) - } - if err := os.WriteFile(outPath, []byte(content), 0644); err != nil { - return fmt.Errorf("write scaffold %s: %w", outPath, err) - } - } + if scaffoldDir == "" { + return nil } + // Scaffold provider — only written if it doesn't already exist, so a + // hand-written provider is never clobbered by a regeneration. + scaffoldServiceDir := filepath.Join(scaffoldDir, pkgName) + if err := os.MkdirAll(scaffoldServiceDir, 0755); err != nil { + return fmt.Errorf("mkdir scaffold %s: %w", scaffoldServiceDir, err) + } + outPath := filepath.Join(scaffoldServiceDir, "provider.go") + if _, err := os.Stat(outPath); err == nil { + return nil + } + content, err := g.GenerateScaffold(pkgName, model) + if err != nil { + return fmt.Errorf("generate scaffold %s: %w", outPath, err) + } + if err := WriteGo(outPath, content); err != nil { + return fmt.Errorf("write scaffold %s: %w", outPath, err) + } return nil } diff --git a/scripts/download-smithy-models.sh b/scripts/download-smithy-models.sh index d10bdbec..6fcf4a51 100755 --- a/scripts/download-smithy-models.sh +++ b/scripts/download-smithy-models.sh @@ -1,55 +1,75 @@ #!/usr/bin/env bash # scripts/download-smithy-models.sh -# Downloads AWS Smithy JSON models from aws-sdk-go-v2 repository. +# Downloads AWS Smithy JSON models from the aws-sdk-go-v2 repository. +# +# The models committed under smithy-models/ are the pin for `make codegen`: +# BASE_URL tracks aws-sdk-go-v2 *main*, so without them codegen output would +# depend on whatever upstream published that day. By default this script only +# fills in missing files, which keeps a local codegen run reproducible and +# offline. The weekly smithy-sync workflow passes --refresh so upstream changes +# land as a reviewable model diff next to the regenerated code. +# +# usage: download-smithy-models.sh [--refresh] [models-dir] [service...] +# Service names are upstream model filenames without .json. With none given, +# every model already in models-dir is processed — name a service explicitly +# to add a new one. set -euo pipefail +REFRESH=0 +if [ "${1:-}" = "--refresh" ]; then + REFRESH=1 + shift +fi + MODELS_DIR="${1:-./smithy-models}" +shift || true BASE_URL="https://raw.githubusercontent.com/aws/aws-sdk-go-v2/main/codegen/sdk-codegen/aws-models" -# Complete list of services to download, organized by DevCloud tiers. -SERVICES=( - # Existing - s3 sqs dynamodb lambda iam sts - # Tier 1 - sns kms secretsmanager ssm logs monitoring events ec2 ecs ecr route53 acm - # Tier 2 - cognito-identity cognito-identity-provider elasticloadbalancingv2 ebs efs - sfn apigateway apigatewayv2 kinesis firehose ses sesv2 rds cloudformation - # Tier 3 - elasticache cloudfront wafv2 glue athena organizations cloudtrail eks - autoscaling appsync emr batch - # Tier 4 (extended coverage) - amplify appconfig application-auto-scaling backup bedrock - cloud-control cloudsearch - codeartifact codebuild codecommit codedeploy codepipeline - config-service cost-explorer dms docdb dynamodb-streams - elastic-beanstalk elasticsearch-service - eventbridge-pipes fis glacier identity-store - iot iot-data-plane iot-wireless - kafka kinesis-analytics-v2 lakeformation - managedblockchain memorydb mq mwaa neptune - opensearch pinpoint acm-pca redshift - ram resource-groups resource-groups-tagging-api - route53resolver sagemaker - serverlessrepo servicediscovery shield - sso-admin support swf textract - timestream-write transcribe transfer - verified-permissions waf xray -) - mkdir -p "$MODELS_DIR" +# Deriving the service list from the directory keeps it from drifting out of +# sync with the models we actually generate from — a hand-maintained list grows +# entries whose upstream filename has since changed, and those 404 silently. +if [ "$#" -gt 0 ]; then + SERVICES=("$@") +else + SERVICES=() + for f in "$MODELS_DIR"/*.json; do + [ -e "$f" ] || continue + SERVICES+=("$(basename "$f" .json)") + done +fi + +if [ "${#SERVICES[@]}" -eq 0 ]; then + echo "No models in $MODELS_DIR and no service named; pass service names to seed it." >&2 + exit 1 +fi + +updated=0 +failed=0 for service in "${SERVICES[@]}"; do dest="${MODELS_DIR}/${service}.json" - if [ -f "$dest" ]; then + if [ -f "$dest" ] && [ "$REFRESH" -eq 0 ]; then echo "SKIP $service (exists)" continue fi - echo "Downloading $service..." - curl -sfL "${BASE_URL}/${service}.json" -o "$dest" 2>/dev/null || { - echo "WARN: failed to download $service, skipping" - rm -f "$dest" - } + # Download to a temp file first: a failed refresh must never delete or + # truncate a model that is already committed. + tmp="${dest}.tmp" + if curl -sfL "${BASE_URL}/${service}.json" -o "$tmp"; then + if [ -f "$dest" ] && cmp -s "$tmp" "$dest"; then + rm -f "$tmp" + else + mv "$tmp" "$dest" + updated=$((updated + 1)) + echo "UPDATED $service" + fi + else + rm -f "$tmp" + failed=$((failed + 1)) + echo "WARN: failed to download $service, keeping any existing copy" >&2 + fi done -echo "Done. $(ls "$MODELS_DIR"/*.json 2>/dev/null | wc -l | tr -d ' ') models downloaded." +total=$(ls "$MODELS_DIR"/*.json 2>/dev/null | wc -l | tr -d ' ') +echo "Done. ${updated} updated, ${failed} failed, ${total} models total." From e79ea8934c1faeb1c26ef5a0d6549849ced84c4c Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Thu, 30 Jul 2026 08:00:59 +0900 Subject: [PATCH 5/8] docs: correct the generated-code surface and the admin API The codegen diagram listed interface.go, serializer.go, and deserializer.go as generated outputs. None of them exist, and README claimed codegen produces "serializers". That is the source of a recurring misreading of internal/generated as scaffolding waiting to be filled in: there is no generated wire glue, providers parse the raw *http.Request themselves, so types.go and base_provider.go have nothing to connect to and only router.go is consumed today. Document the four files actually generated, and why the Smithy models are committed. Also drop the event bus and WebSocket sections, the GetMetrics contract row, the /devcloud/api/metrics endpoint, and the auth.enabled key, all of which describe code that no longer exists; and state that the services config block is optional and authoritative. --- README.md | 4 ++-- docs/architecture.md | 39 ++++++++++++++++++--------------------- docs/configuration.md | 28 +++++++++++++--------------- docs/getting-started.md | 4 +--- docs/plugin-api.md | 5 +---- docs/troubleshooting.md | 1 - 6 files changed, 35 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index ae12d9a7..a6643fc2 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ Today DevCloud targets **AWS**. Our long-term goal is to support the full range - **104 AWS services** across all major categories (run `make stats` for current counts; see [services-matrix.md](docs/services-matrix.md)) - **boto3-compatible** — a 729-test compatibility suite runs in CI (`make test-compat`); works with most boto3 apps, and unsupported operations return a clean AWS error, never a false success - **Cross-service integration** — CFN provisioning, DDB Streams → Lambda, EventBridge targets, S3 → Lambda -- **Smithy-driven codegen** — auto-generate Go interfaces, types, and serializers from Smithy models +- **Smithy-driven codegen** — auto-generate Go types, routers, and error catalogues from Smithy models - **Weekly auto-sync** — GitHub Actions keeps generated code up to date with upstream AWS API changes - **Single binary, zero-config** — one Docker image, one port (4747), no config file required (embedded defaults) - **Environment variable overrides** — `DEVCLOUD_SERVICES`, `DEVCLOUD_DATA_DIR`, `DEVCLOUD_PORT` for quick configuration without YAML - **SDK/CLI compatible** — works with the AWS SDK, CLI, Terraform, CDK out of the box -- **Admin API** — opt-in REST + WebSocket at `/devcloud/api/*` for service status, resource listing, and live request logs (`admin.enabled: true`). The web dashboard UI lives in a separate repository. +- **Admin API** — opt-in REST at `/devcloud/api/*` for service status, resource listing, and recent request logs (`admin.enabled: true`). The web dashboard UI lives in a separate repository. ## Quick Start diff --git a/docs/architecture.md b/docs/architecture.md index 8acf0a44..0376f4dc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -65,7 +65,6 @@ type ServicePlugin interface { Shutdown(ctx context.Context) error HandleRequest(ctx context.Context, op string, req *http.Request) (*Response, error) ListResources(ctx context.Context) ([]Resource, error) - GetMetrics(ctx context.Context) (*ServiceMetrics, error) } ``` @@ -102,12 +101,16 @@ smithy-models/*.json (AWS Smithy model files) Uses Go templates to produce: │ ├─ types.go — Request/response structs - ├─ interface.go — Service interface (all operations) - ├─ serializer.go — Request marshaling - ├─ deserializer.go — Response unmarshaling - ├─ router.go — Operation routing + ├─ router.go — Method+URI → operation routing (REST services) ├─ errors.go — Service-specific error types └─ base_provider.go — Stub implementation (NotImplementedError) + + There is no generated serializer: providers receive the raw + *http.Request and parse it themselves (map[string]any for JSON + protocols), so types.go/base_provider.go have no wire glue and only + router.go is consumed today — by the REST services that need + MatchOperation. See docs/crud-engine.md for how the long tail of + unimplemented operations is actually served. │ ▼ internal/generated/{service}/ (DO NOT EDIT) @@ -126,24 +129,20 @@ make codegen-s3 ### Weekly Auto-Sync A GitHub Actions workflow runs weekly to: -1. Fetch the latest Smithy models from AWS +1. Re-download the Smithy models (`scripts/download-smithy-models.sh --refresh`) 2. Run codegen -3. Open a PR if any generated code changed - -## Event Bus +3. Open a PR if the models or the generated code changed -The event bus (`internal/eventbus/`) provides in-memory pub/sub for internal communication: - -- Services publish events (resource created, deleted, etc.) -- The admin API subscribes to stream real-time updates via WebSocket -- Loose coupling between services and the admin API +The models under `smithy-models/` are committed on purpose: the download URL +tracks `aws-sdk-go-v2` *main*, so they are the pin that makes `make codegen` +reproducible and offline. Only the weekly job passes `--refresh`, which is what +makes an upstream API change show up as a reviewable model diff next to the +regenerated code. ## Admin API -The admin API (`internal/admin/`) provides: - -- **REST API** at `/devcloud/api/` — service status, resource listing, request logs -- **WebSocket** at `/devcloud/api/ws` — real-time event streaming +The admin API (`internal/admin/`) provides a **REST API** at `/devcloud/api/` — +service status, resource listing, and recent request logs. It is disabled by default (`admin.enabled: false`). The web dashboard UI is a separate project (its own repository) that consumes this API; the Go server @@ -165,15 +164,13 @@ devcloud/ │ ├── config/ # YAML config loading, env overrides │ ├── generated/ # Auto-generated code (DO NOT EDIT; run `make stats` for count) │ ├── services/ # Service implementations (run `make stats` for count) -│ ├── admin/ # Admin REST API + WebSocket -│ ├── eventbus/ # In-memory event pub/sub +│ ├── admin/ # Admin REST API │ └── storage/ # Shared storage abstractions ├── docker/ # Dockerfile, docker-compose.yml ├── smithy-models/ # AWS Smithy JSON model files ├── tests/compatibility/ # Python/boto3 compatibility tests ├── docs/ # Documentation ├── Makefile -├── devcloud.yaml # Default configuration └── go.mod ``` diff --git a/docs/configuration.md b/docs/configuration.md index a955f7f8..36ebe9b9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration -**Configuration is optional.** DevCloud ships with built-in defaults: running `devcloud` with no flags enables all 101 services on port 4747, storing data under `./data//`. The embedded defaults are compiled into the binary from [`internal/config/default.yaml`](https://github.com/skyoo2003/devcloud/blob/main/internal/config/default.yaml). +**Configuration is optional.** DevCloud ships with built-in defaults: running `devcloud` with no flags enables every registered service on port 4747, storing data under `./data//` (run `make stats` for the current service count). The embedded defaults are compiled into the binary from [`internal/config/default.yaml`](https://github.com/skyoo2003/devcloud/blob/main/internal/config/default.yaml). To override defaults, provide a YAML file. DevCloud looks for config in this order: @@ -26,12 +26,14 @@ Environment variables override YAML values for selected keys (see [Environment V ### Services -Each service has the following options: +The `services` block is **optional and authoritative**: omit it (as the embedded +default does) and every registered service starts with `data_dir +./data/`; list any service and *only* the services you list start. | Key | Default | Description | |-----|---------|-------------| | `services..enabled` | `true` | Enable or disable the service | -| `services..data_dir` | varies | Data directory for persistent storage | +| `services..data_dir` | `./data/` | Data directory for persistent storage | Service-specific options: @@ -41,20 +43,17 @@ Service-specific options: | `services.lambda.warm_containers` | `0` | Number of warm containers to keep | | `services.iam.enforce_policies` | `false` | Enforce IAM policies (experimental) | -### Auth - -| Key | Default | Description | -|-----|---------|-------------| -| `auth.enabled` | `false` | Enable SigV4 signature validation | - ### Admin API | Key | Default | Description | |-----|---------|-------------| -| `admin.enabled` | `false` | Enable the admin REST + WebSocket API at `/devcloud/api/*` | +| `admin.enabled` | `false` | Enable the admin REST API at `/devcloud/api/*` | > The `dashboard` key was renamed to `admin`. The old `dashboard.enabled` key is still honoured for one release (with a deprecation warning); migrate to `admin.enabled`. +> SigV4 signature validation is not implemented, so there is no `auth` key. Any +> credentials are accepted. + ### Logging | Key | Default | Description | @@ -68,6 +67,8 @@ Service-specific options: server: port: 4747 +# Optional: listing services restricts startup to exactly this set. +# Omit the whole block to run every registered service. services: s3: enabled: true @@ -87,9 +88,6 @@ services: enabled: true data_dir: ./data/lambda -auth: - enabled: false - admin: enabled: false @@ -114,7 +112,7 @@ docker run -p 8080:8080 -e DEVCLOUD_PORT=8080 ghcr.io/skyoo2003/devcloud:latest ### `DEVCLOUD_SERVICES` -Comma-separated list of services to enable. When set, **only** the listed services are enabled — all others are disabled regardless of their `enabled` setting in YAML. When not set, each service uses its YAML `enabled` value (or the embedded default of `true`). +Comma-separated list of services to enable. When set, **only** the listed services are enabled — all others are disabled regardless of their `enabled` setting in YAML. When not set, each service uses its YAML `enabled` value (or the embedded default of `true`). An unknown `tierN` token is treated as a literal service name and logged as a warning. **Tier shortcuts** (expand to predefined service groups — see [`internal/config/config.go`](https://github.com/skyoo2003/devcloud/blob/main/internal/config/config.go) for the exact list): @@ -143,7 +141,7 @@ docker run -p 4747:4747 -e DEVCLOUD_SERVICES=tier1 ghcr.io/skyoo2003/devcloud:la ### `DEVCLOUD_DATA_DIR` -Overrides the base data directory for **all** services. When set, every service uses `/` — per-service `data_dir` values in YAML are ignored. When not set, each service falls back to its YAML `data_dir` value (or the embedded default). +Overrides the base data directory for **all** services. When set, every service uses `/` — per-service `data_dir` values in YAML are ignored. When not set, each service falls back to its YAML `data_dir` value, or `./data/`. ```bash # Put all service data under /tmp/devcloud-local diff --git a/docs/getting-started.md b/docs/getting-started.md index 064030ef..114709b6 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -94,9 +94,7 @@ is exposed under `/devcloud/api/*`: - `GET /devcloud/api/services` — service status overview - `GET /devcloud/api/services/{id}/resources` — resource browser (buckets, queues, tables, functions) -- `GET /devcloud/api/metrics` — aggregate request/error counters -- `GET /devcloud/api/logs` — recent API call logs -- `GET /devcloud/api/ws` — WebSocket live updates +- `GET /devcloud/api/logs` — recent API call logs (`?limit=`) The web dashboard UI that consumes this API lives in a separate repository. diff --git a/docs/plugin-api.md b/docs/plugin-api.md index f35c6f61..05db2e62 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -20,7 +20,6 @@ type ServicePlugin interface { Shutdown(ctx context.Context) error HandleRequest(ctx context.Context, op string, req *http.Request) (*Response, error) ListResources(ctx context.Context) ([]Resource, error) - GetMetrics(ctx context.Context) (*ServiceMetrics, error) } ``` @@ -35,7 +34,6 @@ type ServicePlugin interface { | `Shutdown(ctx)` | Called once at graceful shutdown (15s budget). Flush/close resources. Idempotent-friendly. | | `HandleRequest(ctx, op, req)` | Handle one API call. `op` is the operation name pre-extracted by the gateway (see [operation names](#operation-names)); it may be `""` for REST protocols, in which case derive it from method+path or `X-Amz-Target`. Return a `*Response`; return an `error` only for unexpected internal failures (the gateway wraps those as a `500 InternalError`). **Model AWS errors as a normal `*Response`** with the right status and error body, not as a Go `error`. | | `ListResources(ctx)` | Return the resources this service currently holds (admin API). May be empty. | -| `GetMetrics(ctx)` | Return request/error/resource counters (admin API). May be zero-valued. | These invariants are enforced for every registered service by [`TestServicePluginConformance`](../cmd/devcloud/conformance_test.go). @@ -130,8 +128,7 @@ The registry also exposes `RegisteredServices()` (all registered IDs) and ## API stability Starting at **v1.0**, `ServicePlugin`, `PluginConfig`, `Response`, `Resource`, -`ServiceMetrics`, and the `ProtocolType` constants are stable within the `v1.x` -series: +and the `ProtocolType` constants are stable within the `v1.x` series: - No method will be **removed** from `ServicePlugin` and no existing method **signature** will change in a `v1.x` release. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 50257085..da387d64 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -67,7 +67,6 @@ By default, DevCloud accepts any signature — it checks the SigV4 *format* but - Your client is configured with dummy but **non-empty** credentials (`aws_access_key_id="test"`, `aws_secret_access_key="test"`). - The `endpoint_url` points to DevCloud, not real AWS. -- If you enabled `auth.enabled: true`, either disable it or provide the credentials you registered. ### `NoSuchBucket` / `ResourceNotFoundException` after restart From 6734a9bba1e680330a097df31fe0ab69a6d2db04 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Thu, 30 Jul 2026 08:01:00 +0900 Subject: [PATCH 6/8] chore: run the full test suite in CI, add changelog fragments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI ran go test ./internal/..., which skipped cmd/devcloud — so the ServicePlugin conformance test over every registered service, the thing that enforces the documented plugin contract, never actually ran. Use ./... . `make stats` counted services by parsing the services block out of default.yaml, which no longer lists them; count the service packages instead. --- .github/workflows/ci.yml | 4 +++- Makefile | 2 +- changes/unreleased/Changed-20260730-000001.yaml | 5 +++++ changes/unreleased/Fixed-20260730-000002.yaml | 5 +++++ changes/unreleased/Removed-20260730-000000.yaml | 5 +++++ 5 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 changes/unreleased/Changed-20260730-000001.yaml create mode 100644 changes/unreleased/Fixed-20260730-000002.yaml create mode 100644 changes/unreleased/Removed-20260730-000000.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acc306bf..56572677 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,9 @@ jobs: run: sudo apt-get install -y libsqlite3-dev - name: Run Go tests - run: CGO_ENABLED=1 go test ./internal/... -v + # ./... rather than ./internal/... so cmd/devcloud's ServicePlugin + # conformance test runs in CI too. + run: CGO_ENABLED=1 go test ./... -v env: CGO_ENABLED: "1" diff --git a/Makefile b/Makefile index d60abab8..3714a1c6 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ changelog: @changie batch $(VERSION) && changie merge stats: - @svcs=$$(awk '/^services:/{found=1;next} /^[^ \t]/{found=0} found && /^[ \t]+[a-z][a-z0-9_]+:$$/' internal/config/default.yaml | wc -l | tr -d ' '); \ + @svcs=$$(ls -d internal/services/*/ | wc -l | tr -d ' '); \ ops=$$(grep -r 'case "' internal/services/*/provider.go 2>/dev/null | wc -l | tr -d ' '); \ echo "Services: $$svcs"; \ echo "Operations: $$ops" diff --git a/changes/unreleased/Changed-20260730-000001.yaml b/changes/unreleased/Changed-20260730-000001.yaml new file mode 100644 index 00000000..1354dbd1 --- /dev/null +++ b/changes/unreleased/Changed-20260730-000001.yaml @@ -0,0 +1,5 @@ +kind: Changed +body: The `services` config block is now optional. Omit it — as the embedded default now does — and every registered service starts with `data_dir ./data/`; listing services still restricts startup to exactly that set. This replaces a 325-line default file in which all 103 entries were identical boilerplate. +time: 2026-07-30T00:00:01.000000+09:00 +custom: + Issue: "120" diff --git a/changes/unreleased/Fixed-20260730-000002.yaml b/changes/unreleased/Fixed-20260730-000002.yaml new file mode 100644 index 00000000..c28493ec --- /dev/null +++ b/changes/unreleased/Fixed-20260730-000002.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: The weekly Smithy model sync could never report an update. `download-smithy-models.sh` skipped every model already in the tree (all of them), and its hand-maintained service list had drifted so that 14 entries 404'd upstream. The list is now derived from the models present, the workflow passes `--refresh` to re-download them, downloads are written atomically so a failed fetch can no longer delete a committed model, and the workflow's change check uses `git status --porcelain` so a newly generated package is no longer silently dropped as an untracked file. +time: 2026-07-30T00:00:02.000000+09:00 +custom: + Issue: "120" diff --git a/changes/unreleased/Removed-20260730-000000.yaml b/changes/unreleased/Removed-20260730-000000.yaml new file mode 100644 index 00000000..5c965366 --- /dev/null +++ b/changes/unreleased/Removed-20260730-000000.yaml @@ -0,0 +1,5 @@ +kind: Removed +body: Remove dead surface found by a repo-wide over-engineering audit — the in-memory event bus and the admin WebSocket at `/devcloud/api/ws` (no code could ever publish an event, so the socket accepted connections and sent nothing), the `GetMetrics`/`ServiceMetrics` plugin API and the `/devcloud/api/metrics` endpoints (100 of 103 services returned zero-valued metrics; resource counts remain on `/devcloud/api/services`), the unused generic `shared.ResourceStore`, `gateway.ExtractAccountID` (it returned a constant and had no callers), and the `auth.enabled` key whose only effect was a startup warning that it was unimplemented. Drops the `gorilla/websocket` dependency. +time: 2026-07-30T00:00:00.000000+09:00 +custom: + Issue: "120" From 774fc1ac49ff94ff19474d35115fb6e479dc8f2e Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Thu, 30 Jul 2026 08:29:36 +0900 Subject: [PATCH 7/8] fix: unsigned Query requests all routed to SQS, and other audit fallout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A code review of the refactor commits on this branch turned up one behaviour regression and four silent failures. serviceFromQueryRequest dropped its Action fallback on the reasoning that every SDK, the CLI, and Terraform sign their requests. They do — but the fallback only ever ran for a request with neither a SigV4 credential scope nor an iam/sts/sqs host prefix, and for those the function now returned "sqs" unconditionally: an unsigned Action=GetCallerIdentity POST to a bare endpoint reached the SQS provider. The existing Query tests set Host to iam./sts., so they never covered the path they were meant to. The fallback is back, ordered after SigV4 and the host prefix and matching on IAM entity substrings rather than the old 35-name whitelist, which makes it shorter and wider — DeleteRole and ListPolicies were both missing from that list. sqs stays the final default. download-smithy-models.sh counted failures and exited 0, so the weekly sync would regenerate from stale models, see no diff, and report a successful sync of nothing — the same silent no-op the previous commit set out to fix. It now exits 1 when any download failed. Its model count also moves from `ls *.json` to find: under set -euo pipefail a glob matching nothing made ls exit 2 and killed the script just before it printed the summary. The removed auth block is parsed again, only to warn. yaml.Unmarshal ignores unknown keys, so an operator who wrote auth.enabled: true to require signature validation got no warning that nothing checks signatures — the one deprecation that must not be silent. Follows the dashboard key's one-release shim. A services block is authoritative and Enabled is a plain bool, so a block that lists a service without enabled: true starts zero services with nothing in the log. main now warns when the active set is empty, which also covers a typo'd DEVCLOUD_SERVICES. docs/configuration.md claimed enabled defaults to true; it defaults to false and is required per entry. Also: make stats counted service directories and reported 103, because STS lives in internal/services/iam — count registry registrations instead, which gives the 104 README states. Finish the shared.StrParam de-duplication with StrParamDefault, removing five more copies (29 call sites). Drop the now-unreachable "simpleWorkflowService" case label, since the switch lowercases its input. Stop copying the request body into a string to test for "Action=". Merge the split import groups left in twelve providers. Isolate DEVCLOUD_* from the process environment in the config tests, which otherwise fail on a machine that exports them. Left alone deliberately: the eleven services with no committed Smithy model (account, cloudcontrol, dms, ...). The script takes an arbitrary MODELS_DIR and must not know about internal/services; the invariant that every service package has a model belongs in a codegen test. intParam stays duplicated — two of its six copies differ in signature and integer width, so folding them would change behaviour at the call site. --- Makefile | 2 +- .../unreleased/Removed-20260730-000000.yaml | 2 +- cmd/devcloud/main.go | 7 +++ docs/configuration.md | 2 +- internal/config/config.go | 27 ++++++++++++ internal/config/config_test.go | 39 ++++++++++++++++ internal/gateway/protocol.go | 41 +++++++++++++---- internal/gateway/protocol_test.go | 44 +++++++++++++++++++ internal/services/account/provider.go | 1 - internal/services/acm/provider.go | 1 - internal/services/acmpca/provider.go | 1 - internal/services/amplify/provider.go | 15 ++----- internal/services/apigatewayv2/provider.go | 25 ++++------- internal/services/appsync/provider.go | 15 ++----- internal/services/batch/provider.go | 27 +++++------- internal/services/cognitoidentity/provider.go | 1 - internal/services/ec2/provider.go | 1 - internal/services/ecr/provider.go | 1 - internal/services/ecs/provider.go | 1 - internal/services/iotwireless/provider.go | 12 +---- internal/services/kms/provider.go | 1 - internal/services/lambda/provider.go | 1 - internal/services/secretsmanager/provider.go | 1 - internal/services/sns/provider.go | 1 - internal/services/sqs/provider.go | 1 - internal/shared/params.go | 9 ++++ scripts/download-smithy-models.sh | 13 +++++- 27 files changed, 203 insertions(+), 89 deletions(-) diff --git a/Makefile b/Makefile index 3714a1c6..4d773a83 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ changelog: @changie batch $(VERSION) && changie merge stats: - @svcs=$$(ls -d internal/services/*/ | wc -l | tr -d ' '); \ + @svcs=$$(grep -rho 'DefaultRegistry.Register(' internal/services | wc -l | tr -d ' '); \ ops=$$(grep -r 'case "' internal/services/*/provider.go 2>/dev/null | wc -l | tr -d ' '); \ echo "Services: $$svcs"; \ echo "Operations: $$ops" diff --git a/changes/unreleased/Removed-20260730-000000.yaml b/changes/unreleased/Removed-20260730-000000.yaml index 5c965366..3f6a786a 100644 --- a/changes/unreleased/Removed-20260730-000000.yaml +++ b/changes/unreleased/Removed-20260730-000000.yaml @@ -1,5 +1,5 @@ kind: Removed -body: Remove dead surface found by a repo-wide over-engineering audit — the in-memory event bus and the admin WebSocket at `/devcloud/api/ws` (no code could ever publish an event, so the socket accepted connections and sent nothing), the `GetMetrics`/`ServiceMetrics` plugin API and the `/devcloud/api/metrics` endpoints (100 of 103 services returned zero-valued metrics; resource counts remain on `/devcloud/api/services`), the unused generic `shared.ResourceStore`, `gateway.ExtractAccountID` (it returned a constant and had no callers), and the `auth.enabled` key whose only effect was a startup warning that it was unimplemented. Drops the `gorilla/websocket` dependency. +body: Remove dead surface found by a repo-wide over-engineering audit — the in-memory event bus and the admin WebSocket at `/devcloud/api/ws` (no code could ever publish an event, so the socket accepted connections and sent nothing), the `GetMetrics`/`ServiceMetrics` plugin API and the `/devcloud/api/metrics` endpoints (100 of 103 services returned zero-valued metrics; resource counts remain on `/devcloud/api/services`), the unused generic `shared.ResourceStore`, `gateway.ExtractAccountID` (it returned a constant and had no callers), and the `auth.enabled` key whose only effect was a startup warning that it was unimplemented (the key is still read for one release so that enabling it warns that credentials are accepted regardless, rather than being ignored in silence). Drops the `gorilla/websocket` dependency. time: 2026-07-30T00:00:00.000000+09:00 custom: Issue: "120" diff --git a/cmd/devcloud/main.go b/cmd/devcloud/main.go index 73b3433c..0719dab7 100644 --- a/cmd/devcloud/main.go +++ b/cmd/devcloud/main.go @@ -94,6 +94,13 @@ func main() { initService(name, false) } + // A services block is authoritative and `enabled` defaults to Go's false, + // so `services:\n s3:\n` (or a typo'd DEVCLOUD_SERVICES) silently brings up + // nothing. Serving zero services is never what an operator wanted. + if len(registry.ActiveServices()) == 0 { + slog.Warn("no services enabled; a 'services' block only starts what it lists, and each entry still needs 'enabled: true' — also check DEVCLOUD_SERVICES") + } + // Admin API: build the REST handler only when the operator opted in via // admin.enabled. Otherwise expose a 404 handler so the admin routes don't // leak service internals. This binary serves no web UI; the dashboard diff --git a/docs/configuration.md b/docs/configuration.md index 36ebe9b9..5cddca2b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -32,7 +32,7 @@ default does) and every registered service starts with `data_dir | Key | Default | Description | |-----|---------|-------------| -| `services..enabled` | `true` | Enable or disable the service | +| `services..enabled` | `false` | Enable the service. **Required per entry** — listing a service is not enough, `enabled: true` still has to be set. (With no `services` block at all, every service is enabled.) | | `services..data_dir` | `./data/` | Data directory for persistent storage | Service-specific options: diff --git a/internal/config/config.go b/internal/config/config.go index b04c983f..8a58fa89 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -36,6 +36,13 @@ type Config struct { // block from an absent one and apply the correct precedence. Dashboard *AdminConfig `yaml:"dashboard"` + // Auth is the removed pre-1.0 auth block. SigV4 enforcement was never + // implemented, so the key is gone — but yaml.Unmarshal ignores unknown keys + // silently, and an operator who wrote auth.enabled: true to require + // signature validation must not be left believing it took effect. Parsed + // only to warn (see parse); remove with the dashboard key. + Auth *AuthConfig `yaml:"auth"` + // allowed is the DEVCLOUD_SERVICES filter; nil means no filtering. allowed map[string]bool // baseDir is the DEVCLOUD_DATA_DIR override; "" means honour data_dir. @@ -55,6 +62,12 @@ type AdminConfig struct { Enabled bool `yaml:"enabled"` } +// AuthConfig is the shape of the removed auth block, kept only so parse can +// warn about it instead of ignoring it. See Config.Auth. +type AuthConfig struct { + Enabled bool `yaml:"enabled"` +} + type LoggingConfig struct { Level string `yaml:"level"` Format string `yaml:"format"` @@ -135,6 +148,20 @@ func parse(data []byte) (*Config, []string, error) { } cfg.Dashboard = nil } + + // The auth block is gone, and an ignored auth.enabled: true is a security + // surprise: the operator thinks signatures are checked when nothing is. + if cfg.Auth != nil { + if cfg.Auth.Enabled { + warnings = append(warnings, + "config: 'auth.enabled: true' has no effect — SigV4 enforcement is not implemented and the 'auth' key was removed; any credentials are accepted") + } else { + warnings = append(warnings, + "config: the 'auth' key was removed and is ignored; delete it") + } + cfg.Auth = nil + } + if cfg.Admin == nil { cfg.Admin = &AdminConfig{} } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2bddebb2..93c2b0dd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -47,7 +47,19 @@ func TestExpandTiers_UnknownToken_TreatedAsService(t *testing.T) { assert.Contains(t, warnings[0], "tier9") } +// isolateEnv clears the env overrides parse() reads, so a developer or CI runner +// that exports DEVCLOUD_DATA_DIR / DEVCLOUD_SERVICES / DEVCLOUD_PORT does not +// fail every assertion about ports, data dirs, and enabled services. +// t.Setenv restores the previous value when the test ends. +func isolateEnv(t *testing.T) { + t.Helper() + for _, k := range []string{"DEVCLOUD_DATA_DIR", "DEVCLOUD_SERVICES", "DEVCLOUD_PORT"} { + t.Setenv(k, "") + } +} + func TestService_EnvServiceFilter(t *testing.T) { + isolateEnv(t) cfg := &Config{ Services: map[string]ServiceConfig{ "s3": {Enabled: true}, @@ -64,6 +76,7 @@ func TestService_EnvServiceFilter(t *testing.T) { // contract: with no services block every service is enabled and lands under // ./data/. func TestService_NoServicesBlock_EnablesEverything(t *testing.T) { + isolateEnv(t) cfg, _, err := parse([]byte("server:\n port: 4747\n")) require.NoError(t, err) @@ -78,6 +91,7 @@ func TestService_NoServicesBlock_EnablesEverything(t *testing.T) { // restricts startup to that list — otherwise a minimal config would silently // start all 100+ services. func TestService_ExplicitBlockIsAuthoritative(t *testing.T) { + isolateEnv(t) cfg, _, err := parse([]byte("services:\n s3:\n enabled: true\n")) require.NoError(t, err) assert.True(t, cfg.Service("s3").Enabled) @@ -85,6 +99,7 @@ func TestService_ExplicitBlockIsAuthoritative(t *testing.T) { } func TestService_DataDirOverride(t *testing.T) { + isolateEnv(t) yaml := []byte("services:\n s3:\n enabled: true\n data_dir: ./custom/s3\n") cfg, _, err := parse(yaml) @@ -99,6 +114,7 @@ func TestService_DataDirOverride(t *testing.T) { } func TestLoadConfig_DefaultFile(t *testing.T) { + isolateEnv(t) dir := t.TempDir() cfgPath := filepath.Join(dir, "devcloud.yaml") err := os.WriteFile(cfgPath, []byte(` @@ -130,6 +146,7 @@ logging: // when the fallback path doesn't exist, LoadOrDefault returns the embedded // default configuration (port 4747, every service enabled). func TestLoadOrDefault_FileMissing_UsesEmbedded(t *testing.T) { + isolateEnv(t) cfg, _, err := LoadOrDefault(filepath.Join(t.TempDir(), "nonexistent.yaml")) require.NoError(t, err) require.NotNil(t, cfg) @@ -141,6 +158,7 @@ func TestLoadOrDefault_FileMissing_UsesEmbedded(t *testing.T) { // TestLoadOrDefault_FileExists_UsesFile verifies that when the fallback path // exists, its contents are loaded (not the embedded default). func TestLoadOrDefault_FileExists_UsesFile(t *testing.T) { + isolateEnv(t) dir := t.TempDir() cfgPath := filepath.Join(dir, "devcloud.yaml") require.NoError(t, os.WriteFile(cfgPath, []byte(` @@ -161,6 +179,7 @@ services: // TestLoadOrDefault_EmptyPath_UsesEmbedded verifies that an empty fallback // path skips the file check entirely and uses embedded defaults. func TestLoadOrDefault_EmptyPath_UsesEmbedded(t *testing.T) { + isolateEnv(t) cfg, _, err := LoadOrDefault("") require.NoError(t, err) assert.Equal(t, 4747, cfg.Server.Port) @@ -187,6 +206,25 @@ func TestParse_DeprecatedDashboardKey(t *testing.T) { assert.Contains(t, warnings[0], "deprecated") } +// TestParse_RemovedAuthKeyWarns verifies the removed 'auth' key is reported +// rather than silently ignored: yaml.Unmarshal drops unknown keys, so an +// operator who set auth.enabled: true to require SigV4 would otherwise get no +// hint that nothing validates signatures. +func TestParse_RemovedAuthKeyWarns(t *testing.T) { + _, warnings, err := parse([]byte("auth:\n enabled: true\n")) + require.NoError(t, err) + require.Len(t, warnings, 1) + assert.Contains(t, warnings[0], "auth.enabled") + assert.Contains(t, warnings[0], "not implemented") + + // An explicit auth.enabled: false is still stale config worth flagging, but + // it must not claim credentials are being accepted contrary to intent. + _, warnings, err = parse([]byte("auth:\n enabled: false\n")) + require.NoError(t, err) + require.Len(t, warnings, 1) + assert.Contains(t, warnings[0], "removed") +} + // TestParse_AdminKeyWinsOverDeprecated verifies an explicit 'admin' block takes // precedence over the deprecated 'dashboard' key when both are present. func TestParse_AdminKeyWinsOverDeprecated(t *testing.T) { @@ -205,6 +243,7 @@ func TestParse_AdminKeyWinsOverDeprecated(t *testing.T) { // payload yields a Config with at least the default server port populated, // so downstream code sees a usable config rather than a zero-value one. func TestParse_EmptyData_FillsDefaults(t *testing.T) { + isolateEnv(t) cfg, _, err := parse([]byte("")) require.NoError(t, err) assert.Equal(t, 4747, cfg.Server.Port, "empty data should still yield default port 4747") diff --git a/internal/gateway/protocol.go b/internal/gateway/protocol.go index 60e1e32e..3d6a04ec 100644 --- a/internal/gateway/protocol.go +++ b/internal/gateway/protocol.go @@ -6,6 +6,7 @@ import ( "bytes" "io" "net/http" + "net/url" "strings" ) @@ -33,8 +34,8 @@ func DetectProtocol(r *http.Request) (protocol string, serviceID string) { // Restore the body so downstream handlers can read it again. r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - if strings.Contains(string(bodyBytes), "Action=") { - return "query", serviceFromQueryRequest(r) + if bytes.Contains(bodyBytes, []byte("Action=")) { + return "query", serviceFromQueryRequest(r, string(bodyBytes)) } } } @@ -186,7 +187,7 @@ func normalizeServiceID(svc string) string { return "mwaa" case "awssfn", "awsstepfunctions": return "sfn" - case "simpleWorkflowService", "simpleworkflowservice": + case "simpleworkflowservice": return "swf" case "swbexternalservice": return "ssoadmin" @@ -266,11 +267,12 @@ func normalizeServiceID(svc string) string { } // serviceFromQueryRequest determines the service for a Query-protocol request -// from the SigV4 credential scope, falling back to the Host header prefix. -// Every AWS SDK, CLI, and Terraform request is signed, so the credential scope -// carries the signing name; sqs is the default for the unsigned, unprefixed -// case because it is the only Query service SDKs address by bare endpoint. -func serviceFromQueryRequest(r *http.Request) string { +// from the SigV4 credential scope, then the Host header prefix, then the Action +// parameter. SDKs, the CLI, and Terraform all sign, so the credential scope +// almost always decides it — but an unsigned client posting to a bare endpoint +// has neither a scope nor a host prefix, and defaulting those to sqs sends +// every IAM and STS call to the wrong provider. sqs remains the final default. +func serviceFromQueryRequest(r *http.Request, body string) string { if svc := serviceFromSigV4(r); svc != "" { return svc } @@ -281,5 +283,28 @@ func serviceFromQueryRequest(r *http.Request) string { case "sts": return "sts" } + + values, err := url.ParseQuery(body) + if err != nil { + return "sqs" + } + action := values.Get("Action") + switch { + case action == "GetCallerIdentity", action == "GetSessionToken", + action == "GetFederationToken", strings.HasPrefix(action, "AssumeRole"): + return "sts" + case values.Get("QueueUrl") != "", values.Get("QueueName") != "", + action == "ListQueues": + return "sqs" + case action != "": + // Every SQS operation names a queue, so it is fully covered above. + // What is left that talks about IAM entities is IAM. "Polic" rather than + // "Policy" so the plural in ListPolicies/DeletePolicies still matches. + for _, entity := range []string{"User", "Role", "Polic", "Group", "AccessKey"} { + if strings.Contains(action, entity) { + return "iam" + } + } + } return "sqs" } diff --git a/internal/gateway/protocol_test.go b/internal/gateway/protocol_test.go index 4e18597c..fd905f43 100644 --- a/internal/gateway/protocol_test.go +++ b/internal/gateway/protocol_test.go @@ -63,6 +63,50 @@ func TestDetectProtocol_Query_IAM(t *testing.T) { assert.Equal(t, "iam", service) } +// TestDetectProtocol_Query_Unsigned_BareHost covers the client that has neither +// a SigV4 credential scope nor a service host prefix (curl, an SDK configured +// unsigned, a test harness). Without the Action fallback every one of these +// routes to sqs and gets the wrong provider's error. +func TestDetectProtocol_Query_Unsigned_BareHost(t *testing.T) { + cases := []struct{ name, body, want string }{ + {"sts_caller_identity", "Action=GetCallerIdentity", "sts"}, + {"sts_assume_role", "Action=AssumeRoleWithWebIdentity&RoleArn=x", "sts"}, + {"sts_session_token", "Action=GetSessionToken", "sts"}, + {"iam_create_user", "Action=CreateUser&UserName=test", "iam"}, + {"iam_delete_role", "Action=DeleteRole&RoleName=r", "iam"}, + {"iam_list_policies", "Action=ListPolicies", "iam"}, + {"iam_create_access_key", "Action=CreateAccessKey&UserName=u", "iam"}, + {"sqs_send", "Action=SendMessage&QueueUrl=http://x/q&MessageBody=hi", "sqs"}, + {"sqs_create", "Action=CreateQueue&QueueName=q", "sqs"}, + {"sqs_list", "Action=ListQueues", "sqs"}, + // An unrecognised Query action still defaults to sqs, as before. + {"unknown_defaults_sqs", "Action=DescribeDBInstances", "sqs"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + req := httptest.NewRequest("POST", "/", strings.NewReader(c.body)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Host = "localhost:4747" + proto, service := DetectProtocol(req) + assert.Equal(t, "query", proto) + assert.Equal(t, c.want, service) + }) + } +} + +// TestDetectProtocol_Query_SigV4WinsOverAction confirms the credential scope +// still decides for signed clients, so the Action fallback cannot misroute the +// SDK/CLI/Terraform traffic that makes up every real request. +func TestDetectProtocol_Query_SigV4WinsOverAction(t *testing.T) { + req := httptest.NewRequest("POST", "/", strings.NewReader("Action=CreateUser&UserName=test")) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", + "AWS4-HMAC-SHA256 Credential=AKIA/20130524/us-east-1/sts/aws4_request, Signature=abc") + req.Host = "localhost:4747" + _, service := DetectProtocol(req) + assert.Equal(t, "sts", service, "credential scope must outrank the Action name") +} + func TestDetectProtocol_Query_STS(t *testing.T) { req := httptest.NewRequest("POST", "/", strings.NewReader("Action=GetCallerIdentity")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") diff --git a/internal/services/account/provider.go b/internal/services/account/provider.go index 516c8800..1560e690 100644 --- a/internal/services/account/provider.go +++ b/internal/services/account/provider.go @@ -13,7 +13,6 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/acm/provider.go b/internal/services/acm/provider.go index e0997395..41d5638a 100644 --- a/internal/services/acm/provider.go +++ b/internal/services/acm/provider.go @@ -20,7 +20,6 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/acmpca/provider.go b/internal/services/acmpca/provider.go index 4598387f..9fdb6279 100644 --- a/internal/services/acmpca/provider.go +++ b/internal/services/acmpca/provider.go @@ -19,7 +19,6 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/amplify/provider.go b/internal/services/amplify/provider.go index 248c4fdf..26626d19 100644 --- a/internal/services/amplify/provider.go +++ b/internal/services/amplify/provider.go @@ -420,7 +420,7 @@ func (p *Provider) createApp(params map[string]any) (*plugin.Response, error) { Name: name, Description: shared.StrParam(params, "description"), Repository: shared.StrParam(params, "repository"), - Platform: strParamDefault(params, "platform", "WEB"), + Platform: shared.StrParamDefault(params, "platform", "WEB"), IAMRole: shared.StrParam(params, "iamServiceRoleArn"), DefaultDomain: defaultDomain, } @@ -505,9 +505,9 @@ func (p *Provider) createBranch(appID string, params map[string]any) (*plugin.Re AppID: appID, Name: branchName, ARN: arn, - DisplayName: strParamDefault(params, "displayName", branchName), + DisplayName: shared.StrParamDefault(params, "displayName", branchName), Description: shared.StrParam(params, "description"), - Stage: strParamDefault(params, "stage", "NONE"), + Stage: shared.StrParamDefault(params, "stage", "NONE"), Framework: shared.StrParam(params, "framework"), EnableAutoBuild: boolParamDefault(params, "enableAutoBuild", true), } @@ -781,7 +781,7 @@ func (p *Provider) startJob(appID, branchName string, params map[string]any) (*p AppID: appID, BranchName: branchName, ARN: arn, - JobType: strParamDefault(params, "jobType", "RELEASE"), + JobType: shared.StrParamDefault(params, "jobType", "RELEASE"), Status: "SUCCEED", CommitID: shared.StrParam(params, "commitId"), CommitMsg: shared.StrParam(params, "commitMessage"), @@ -984,13 +984,6 @@ func extractPathParam(path, key string) string { return "" } -func strParamDefault(params map[string]any, key, def string) string { - if v, ok := params[key].(string); ok && v != "" { - return v - } - return def -} - func boolParamDefault(params map[string]any, key string, def bool) bool { if v, ok := params[key].(bool); ok { return v diff --git a/internal/services/apigatewayv2/provider.go b/internal/services/apigatewayv2/provider.go index 80b16bc5..ebf7b1c2 100644 --- a/internal/services/apigatewayv2/provider.go +++ b/internal/services/apigatewayv2/provider.go @@ -356,9 +356,9 @@ func (p *Provider) createAPI(params map[string]any) (*plugin.Response, error) { ID: id, ARN: arn, Name: name, - ProtocolType: strParamDefault(params, "protocolType", "HTTP"), + ProtocolType: shared.StrParamDefault(params, "protocolType", "HTTP"), Description: shared.StrParam(params, "description"), - RouteSelection: strParamDefault(params, "routeSelectionExpression", "$request.method $request.path"), + RouteSelection: shared.StrParamDefault(params, "routeSelectionExpression", "$request.method $request.path"), APIEndpoint: apiEndpoint, } @@ -476,7 +476,7 @@ func (p *Provider) createRoute(apiID string, params map[string]any) (*plugin.Res APIID: apiID, RouteKey: routeKey, Target: shared.StrParam(params, "target"), - AuthorizationType: strParamDefault(params, "authorizationType", "NONE"), + AuthorizationType: shared.StrParamDefault(params, "authorizationType", "NONE"), AuthorizerID: shared.StrParam(params, "authorizerId"), } if err := p.store.CreateRoute(r); err != nil { @@ -539,7 +539,7 @@ func (p *Provider) createRouteResponse(apiID, routeID string, params map[string] ID: shared.GenerateID("", 10), APIID: apiID, RouteID: routeID, - RouteResponseKey: strParamDefault(params, "routeResponseKey", "default"), + RouteResponseKey: shared.StrParamDefault(params, "routeResponseKey", "default"), ModelSelectionExpr: shared.StrParam(params, "modelSelectionExpression"), } if err := p.store.CreateRouteResponse(rr); err != nil { @@ -584,10 +584,10 @@ func (p *Provider) createIntegration(apiID string, params map[string]any) (*plug i := &Integration{ ID: shared.GenerateID("", 10), APIID: apiID, - Type: strParamDefault(params, "integrationType", "HTTP_PROXY"), + Type: shared.StrParamDefault(params, "integrationType", "HTTP_PROXY"), IntegrationURI: shared.StrParam(params, "integrationUri"), IntegrationMethod: shared.StrParam(params, "integrationMethod"), - PayloadFormat: strParamDefault(params, "payloadFormatVersion", "2.0"), + PayloadFormat: shared.StrParamDefault(params, "payloadFormatVersion", "2.0"), } if err := p.store.CreateIntegration(i); err != nil { return nil, err @@ -637,7 +637,7 @@ func (p *Provider) createIntegrationResponse(apiID, integrationID string, params ID: shared.GenerateID("", 10), APIID: apiID, IntegrationID: integrationID, - ResponseKey: strParamDefault(params, "integrationResponseKey", "default"), + ResponseKey: shared.StrParamDefault(params, "integrationResponseKey", "default"), TemplateSelection: shared.StrParam(params, "templateSelectionExpression"), } if err := p.store.CreateIntegrationResponse(ir); err != nil { @@ -701,7 +701,7 @@ func (p *Provider) createAuthorizer(apiID string, params map[string]any) (*plugi ID: shared.GenerateID("", 10), APIID: apiID, Name: name, - Type: strParamDefault(params, "authorizerType", "JWT"), + Type: shared.StrParamDefault(params, "authorizerType", "JWT"), IdentitySource: shared.StrParam(params, "identitySource"), JWTConfig: jwtConfig, } @@ -885,7 +885,7 @@ func (p *Provider) createModel(apiID string, params map[string]any) (*plugin.Res ID: shared.GenerateID("", 10), APIID: apiID, Name: name, - ContentType: strParamDefault(params, "contentType", "application/json"), + ContentType: shared.StrParamDefault(params, "contentType", "application/json"), SchemaDef: schemaDef, } if err := p.store.CreateModel(m); err != nil { @@ -1606,13 +1606,6 @@ func extractPathParam(path, key string) string { return "" } -func strParamDefault(params map[string]any, key, def string) string { - if v, ok := params[key].(string); ok && v != "" { - return v - } - return def -} - func boolParamDefault(params map[string]any, key string, def bool) bool { if v, ok := params[key].(bool); ok { return v diff --git a/internal/services/appsync/provider.go b/internal/services/appsync/provider.go index 2c65a67a..d5b3114f 100644 --- a/internal/services/appsync/provider.go +++ b/internal/services/appsync/provider.go @@ -457,7 +457,7 @@ func (p *Provider) createGraphqlApi(params map[string]any) (*plugin.Response, er ID: id, ARN: arn, Name: name, - AuthType: strParamDefault(params, "authenticationType", "API_KEY"), + AuthType: shared.StrParamDefault(params, "authenticationType", "API_KEY"), LogConfig: "{}", Uris: string(urisJSON), } @@ -538,7 +538,7 @@ func (p *Provider) createDataSource(apiID string, params map[string]any) (*plugi ApiID: apiID, Name: name, ARN: arn, - Type: strParamDefault(params, "type", "NONE"), + Type: shared.StrParamDefault(params, "type", "NONE"), Config: "{}", ServiceRole: shared.StrParam(params, "serviceRoleArn"), } @@ -615,7 +615,7 @@ func (p *Provider) createResolver(apiID, typeName string, params map[string]any) DataSource: shared.StrParam(params, "dataSourceName"), RequestTemplate: shared.StrParam(params, "requestMappingTemplate"), ResponseTemplate: shared.StrParam(params, "responseMappingTemplate"), - Kind: strParamDefault(params, "kind", "UNIT"), + Kind: shared.StrParamDefault(params, "kind", "UNIT"), } if err := p.store.CreateResolver(r); err != nil { if isUniqueErr(err) { @@ -827,7 +827,7 @@ func (p *Provider) createType(apiID string, params map[string]any) (*plugin.Resp if name == "" { name = shared.GenerateID("Type", 8) } - format := strParamDefault(params, "format", "SDL") + format := shared.StrParamDefault(params, "format", "SDL") tp := &Type{ ApiID: apiID, Name: name, @@ -1022,13 +1022,6 @@ func extractTagARN(path string) string { return path[idx+len("/tags/"):] } -func strParamDefault(params map[string]any, key, def string) string { - if v, ok := params[key].(string); ok && v != "" { - return v - } - return def -} - func toStringMap(m map[string]any) map[string]string { result := make(map[string]string, len(m)) for k, v := range m { diff --git a/internal/services/batch/provider.go b/internal/services/batch/provider.go index 9a5c2e5b..adb9f437 100644 --- a/internal/services/batch/provider.go +++ b/internal/services/batch/provider.go @@ -298,8 +298,8 @@ func (p *Provider) createComputeEnvironment(params map[string]any) (*plugin.Resp if name == "" { return shared.JSONError("ClientException", "computeEnvironmentName is required", http.StatusBadRequest), nil } - ceType := strParamDefault(params, "type", "MANAGED") - state := strParamDefault(params, "state", "ENABLED") + ceType := shared.StrParamDefault(params, "type", "MANAGED") + state := shared.StrParamDefault(params, "state", "ENABLED") serviceRole := shared.StrParam(params, "serviceRole") computeResources := "{}" @@ -372,8 +372,8 @@ func (p *Provider) updateComputeEnvironment(params map[string]any) (*plugin.Resp if err != nil { return shared.JSONError("ClientException", "compute environment not found", http.StatusBadRequest), nil } - state := strParamDefault(params, "state", ce.State) - serviceRole := strParamDefault(params, "serviceRole", ce.ServiceRole) + state := shared.StrParamDefault(params, "state", ce.State) + serviceRole := shared.StrParamDefault(params, "serviceRole", ce.ServiceRole) if err := p.store.UpdateComputeEnvironment(nameOrARN, state, serviceRole); err != nil { return shared.JSONError("ClientException", "compute environment not found", http.StatusBadRequest), nil @@ -407,7 +407,7 @@ func (p *Provider) createJobQueue(params map[string]any) (*plugin.Response, erro if name == "" { return shared.JSONError("ClientException", "jobQueueName is required", http.StatusBadRequest), nil } - state := strParamDefault(params, "state", "ENABLED") + state := shared.StrParamDefault(params, "state", "ENABLED") priority := int32(0) if v, ok := params["priority"].(float64); ok { priority = int32(v) @@ -484,12 +484,12 @@ func (p *Provider) updateJobQueue(params map[string]any) (*plugin.Response, erro if err != nil { return shared.JSONError("ClientException", "job queue not found", http.StatusBadRequest), nil } - state := strParamDefault(params, "state", jq.State) + state := shared.StrParamDefault(params, "state", jq.State) priority := jq.Priority if v, ok := params["priority"].(float64); ok { priority = int32(v) } - schedulingPolicy := strParamDefault(params, "schedulingPolicyArn", jq.SchedulingPolicy) + schedulingPolicy := shared.StrParamDefault(params, "schedulingPolicyArn", jq.SchedulingPolicy) if err := p.store.UpdateJobQueue(nameOrARN, state, priority, schedulingPolicy); err != nil { return shared.JSONError("ClientException", "job queue not found", http.StatusBadRequest), nil @@ -523,7 +523,7 @@ func (p *Provider) registerJobDefinition(params map[string]any) (*plugin.Respons if name == "" { return shared.JSONError("ClientException", "jobDefinitionName is required", http.StatusBadRequest), nil } - jdType := strParamDefault(params, "type", "container") + jdType := shared.StrParamDefault(params, "type", "container") containerProps := "{}" if cp, ok := params["containerProperties"]; ok { @@ -702,7 +702,7 @@ func (p *Provider) listJobs(params map[string]any) (*plugin.Response, error) { func (p *Provider) cancelJob(params map[string]any) (*plugin.Response, error) { jobID := shared.StrParam(params, "jobId") - reason := strParamDefault(params, "reason", "cancelled") + reason := shared.StrParamDefault(params, "reason", "cancelled") if jobID == "" { return shared.JSONError("ClientException", "jobId is required", http.StatusBadRequest), nil } @@ -714,7 +714,7 @@ func (p *Provider) cancelJob(params map[string]any) (*plugin.Response, error) { func (p *Provider) terminateJob(params map[string]any) (*plugin.Response, error) { jobID := shared.StrParam(params, "jobId") - reason := strParamDefault(params, "reason", "terminated") + reason := shared.StrParamDefault(params, "reason", "terminated") if jobID == "" { return shared.JSONError("ClientException", "jobId is required", http.StatusBadRequest), nil } @@ -991,13 +991,6 @@ func spToDetailMap(sp *SchedulingPolicy, tags map[string]string) map[string]any // --- Util --- -func strParamDefault(params map[string]any, key, def string) string { - if v, ok := params[key].(string); ok && v != "" { - return v - } - return def -} - func toStringMap(m map[string]any) map[string]string { result := make(map[string]string) for k, v := range m { diff --git a/internal/services/cognitoidentity/provider.go b/internal/services/cognitoidentity/provider.go index 1fc41e23..e4fdccd3 100644 --- a/internal/services/cognitoidentity/provider.go +++ b/internal/services/cognitoidentity/provider.go @@ -13,7 +13,6 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/ec2/provider.go b/internal/services/ec2/provider.go index 16be4038..81f4dfd0 100644 --- a/internal/services/ec2/provider.go +++ b/internal/services/ec2/provider.go @@ -15,7 +15,6 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/ecr/provider.go b/internal/services/ecr/provider.go index ad7a57a0..41c18f46 100644 --- a/internal/services/ecr/provider.go +++ b/internal/services/ecr/provider.go @@ -15,7 +15,6 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/ecs/provider.go b/internal/services/ecs/provider.go index b4631f2c..5bc575c7 100644 --- a/internal/services/ecs/provider.go +++ b/internal/services/ecs/provider.go @@ -13,7 +13,6 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/iotwireless/provider.go b/internal/services/iotwireless/provider.go index b42aec8f..3b359e8c 100644 --- a/internal/services/iotwireless/provider.go +++ b/internal/services/iotwireless/provider.go @@ -222,7 +222,7 @@ func (p *Provider) createDestination(params map[string]any) (*plugin.Response, e Name: name, ARN: arn, Expression: shared.StrParam(params, "Expression"), - ExpressionType: strParamDefault(params, "ExpressionType", "RuleName"), + ExpressionType: shared.StrParamDefault(params, "ExpressionType", "RuleName"), RoleARN: shared.StrParam(params, "RoleArn"), Description: shared.StrParam(params, "Description"), } @@ -421,7 +421,7 @@ func (p *Provider) createWirelessDevice(params map[string]any) (*plugin.Response ID: id, ARN: arn, Name: shared.StrParam(params, "Name"), - Type: strParamDefault(params, "Type", "LoRaWAN"), + Type: shared.StrParamDefault(params, "Type", "LoRaWAN"), Destination: shared.StrParam(params, "DestinationName"), Description: shared.StrParam(params, "Description"), Config: configJSON, @@ -998,14 +998,6 @@ func extractPathParam(path, segment string) string { return "" } -func strParamDefault(params map[string]any, key, def string) string { - v, _ := params[key].(string) - if v == "" { - return def - } - return v -} - func isUniqueErr(err error) bool { return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") } diff --git a/internal/services/kms/provider.go b/internal/services/kms/provider.go index 5b17ba71..ef119b66 100644 --- a/internal/services/kms/provider.go +++ b/internal/services/kms/provider.go @@ -15,7 +15,6 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/lambda/provider.go b/internal/services/lambda/provider.go index a5b876ec..74f80b6e 100644 --- a/internal/services/lambda/provider.go +++ b/internal/services/lambda/provider.go @@ -17,7 +17,6 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/secretsmanager/provider.go b/internal/services/secretsmanager/provider.go index c1fb9721..d2cca64b 100644 --- a/internal/services/secretsmanager/provider.go +++ b/internal/services/secretsmanager/provider.go @@ -14,7 +14,6 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/sns/provider.go b/internal/services/sns/provider.go index 10a5a833..2aa93f7e 100644 --- a/internal/services/sns/provider.go +++ b/internal/services/sns/provider.go @@ -14,7 +14,6 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/services/sqs/provider.go b/internal/services/sqs/provider.go index 5913dd1e..fe18aad9 100644 --- a/internal/services/sqs/provider.go +++ b/internal/services/sqs/provider.go @@ -14,7 +14,6 @@ import ( "strings" "github.com/skyoo2003/devcloud/internal/plugin" - "github.com/skyoo2003/devcloud/internal/shared" ) diff --git a/internal/shared/params.go b/internal/shared/params.go index c77ffb27..097d448c 100644 --- a/internal/shared/params.go +++ b/internal/shared/params.go @@ -9,3 +9,12 @@ func StrParam(params map[string]any, key string) string { s, _ := params[key].(string) return s } + +// StrParamDefault returns params[key] as a string, or def when the key is +// absent, holds another type, or holds the empty string. +func StrParamDefault(params map[string]any, key, def string) string { + if s, _ := params[key].(string); s != "" { + return s + } + return def +} diff --git a/scripts/download-smithy-models.sh b/scripts/download-smithy-models.sh index 6fcf4a51..9dc6ca50 100755 --- a/scripts/download-smithy-models.sh +++ b/scripts/download-smithy-models.sh @@ -71,5 +71,16 @@ for service in "${SERVICES[@]}"; do fi done -total=$(ls "$MODELS_DIR"/*.json 2>/dev/null | wc -l | tr -d ' ') +# find, not `ls *.json`: under `set -e` + pipefail a glob that matches nothing +# makes ls exit 2 and kills the script right before it reports what happened. +total=$(find "$MODELS_DIR" -maxdepth 1 -name '*.json' | wc -l | tr -d ' ') echo "Done. ${updated} updated, ${failed} failed, ${total} models total." + +# Exit non-zero on any download failure. Keeping the existing copy is the right +# recovery, but staying green is not: the caller (the weekly sync workflow) would +# regenerate from stale models, find no diff, and report success having synced +# nothing — the exact silent no-op this script was fixed to stop doing. +if [ "$failed" -gt 0 ]; then + echo "ERROR: ${failed} model(s) failed to download; models on disk are unchanged for those." >&2 + exit 1 +fi From 9b42cd948212dfd399234ae3e2b2f0faf634c1c9 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Thu, 30 Jul 2026 20:17:48 +0900 Subject: [PATCH 8/8] docs: condense the unreleased changelog fragments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fragments had grown into commit messages: line counts, per-service tallies, and the reasoning behind each change. A CHANGELOG reader wants what changed and why it mattered; the rest is already in git. Trimmed to one or two sentences each, keeping every name a reader would grep for — config keys, endpoints, package paths, operation names. --- changes/unreleased/Added-20260718-081112.yaml | 2 +- changes/unreleased/Added-20260718-222651.yaml | 2 +- changes/unreleased/Added-20260718-222652.yaml | 2 +- changes/unreleased/Added-20260718-233026.yaml | 2 +- changes/unreleased/Added-20260719-001541.yaml | 2 +- changes/unreleased/Changed-20260724-000001.yaml | 2 +- changes/unreleased/Changed-20260725-000001.yaml | 2 +- changes/unreleased/Changed-20260730-000001.yaml | 2 +- changes/unreleased/Fixed-20260718-222651.yaml | 2 +- changes/unreleased/Fixed-20260718-222652.yaml | 2 +- changes/unreleased/Fixed-20260720-054448.yaml | 2 +- changes/unreleased/Fixed-20260720-055500.yaml | 2 +- changes/unreleased/Fixed-20260730-000002.yaml | 2 +- changes/unreleased/Removed-20260724-000000.yaml | 2 +- changes/unreleased/Removed-20260730-000000.yaml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/changes/unreleased/Added-20260718-081112.yaml b/changes/unreleased/Added-20260718-081112.yaml index a852ff8e..0e60ccb7 100644 --- a/changes/unreleased/Added-20260718-081112.yaml +++ b/changes/unreleased/Added-20260718-081112.yaml @@ -1,5 +1,5 @@ kind: Added -body: Add SQS dead-letter redrive operations (ListDeadLetterSourceQueues, StartMessageMoveTask, ListMessageMoveTasks, CancelMessageMoveTask) — SQS now covers all 23 operations +body: Add the four SQS dead-letter redrive operations (ListDeadLetterSourceQueues, Start/List/CancelMessageMoveTask), completing SQS at 23 operations time: 2026-07-18T08:11:12.483907+09:00 custom: Issue: "94" diff --git a/changes/unreleased/Added-20260718-222651.yaml b/changes/unreleased/Added-20260718-222651.yaml index d05c8912..566d7898 100644 --- a/changes/unreleased/Added-20260718-222651.yaml +++ b/changes/unreleased/Added-20260718-222651.yaml @@ -1,5 +1,5 @@ kind: Added -body: Document the stable `ServicePlugin` plugin API in docs/plugin-api.md (method contracts, config keys, error convention, and the v1.x stability policy), enforced by a conformance test over every registered service +body: Document the `ServicePlugin` API and its v1.x stability policy in docs/plugin-api.md, enforced by a conformance test over every registered service time: 2026-07-18T22:26:51.495862+09:00 custom: Issue: "96" diff --git a/changes/unreleased/Added-20260718-222652.yaml b/changes/unreleased/Added-20260718-222652.yaml index bc82f9d6..16c55c55 100644 --- a/changes/unreleased/Added-20260718-222652.yaml +++ b/changes/unreleased/Added-20260718-222652.yaml @@ -1,5 +1,5 @@ kind: Added -body: Enable and add boto3 compatibility tests for CodeConnections, DMS, and Verified Permissions (previously scaffolded but not enabled) +body: Enable boto3 compatibility tests for CodeConnections, DMS, and Verified Permissions time: 2026-07-18T22:26:52.000000+09:00 custom: Issue: "96" diff --git a/changes/unreleased/Added-20260718-233026.yaml b/changes/unreleased/Added-20260718-233026.yaml index a968c0ac..da7e8cb7 100644 --- a/changes/unreleased/Added-20260718-233026.yaml +++ b/changes/unreleased/Added-20260718-233026.yaml @@ -1,5 +1,5 @@ kind: Added -body: 'Add 24 operations to core services: EC2 instance lifecycle (Start/Stop/Reboot), DescribeAvailabilityZones/DescribeRegions, security group rules (Authorize/Revoke ingress+egress), Elastic IP association, DeleteSecurityGroup/DeleteSubnet/DescribeTags/DeleteTags, and IAM group policy management (attach/detach/put/get/delete/list)' +body: 'Add 24 core-service operations: EC2 instance lifecycle, region/AZ and tag queries, security group rules, Elastic IP association, and IAM group policy management' time: 2026-07-18T23:30:26.666352+09:00 custom: Issue: "97" diff --git a/changes/unreleased/Added-20260719-001541.yaml b/changes/unreleased/Added-20260719-001541.yaml index 5aed7215..247c2680 100644 --- a/changes/unreleased/Added-20260719-001541.yaml +++ b/changes/unreleased/Added-20260719-001541.yaml @@ -1,5 +1,5 @@ kind: Added -body: 'Add a generic Smithy-driven CRUD fallback engine (internal/shared/crud) that auto-serves ~2,200 standard CRUD-shaped operations across all 46 JSON-protocol services with plausible, store-backed responses; hand-implemented operations always take precedence and unclassifiable operations still return an honest error. Documented in docs/crud-engine.md' +body: Serve ~2,200 standard CRUD operations across the 46 JSON-protocol services from a Smithy-driven fallback engine (`internal/shared/crud`); hand-written operations take precedence and unclassifiable ones still return an error. See docs/crud-engine.md time: 2026-07-19T00:15:41.279904+09:00 custom: Issue: "98" diff --git a/changes/unreleased/Changed-20260724-000001.yaml b/changes/unreleased/Changed-20260724-000001.yaml index 43ff3396..7d5967bd 100644 --- a/changes/unreleased/Changed-20260724-000001.yaml +++ b/changes/unreleased/Changed-20260724-000001.yaml @@ -1,5 +1,5 @@ kind: Changed -body: Rename the `internal/dashboard` package to `internal/admin` and the `dashboard.enabled` config key to `admin.enabled`, reflecting its role as a general-purpose admin/introspection API rather than a UI-bound dashboard. The old `dashboard.enabled` key is still honoured for one release with a deprecation warning. +body: Rename `internal/dashboard` to `internal/admin` and the `dashboard.enabled` config key to `admin.enabled`. The old key is honoured for one release with a deprecation warning. time: 2026-07-24T00:00:01.000000+09:00 custom: Issue: "111" diff --git a/changes/unreleased/Changed-20260725-000001.yaml b/changes/unreleased/Changed-20260725-000001.yaml index 4e9aff55..501059cb 100644 --- a/changes/unreleased/Changed-20260725-000001.yaml +++ b/changes/unreleased/Changed-20260725-000001.yaml @@ -1,5 +1,5 @@ kind: Changed -body: Skip request log collection on the AWS request hot path when the admin API is disabled (the default). The log collector is now built only when `admin.enabled` is true, avoiding a per-request mutex lock and ring-buffer write that no consumer could read. +body: Build the request log collector only when `admin.enabled` is set, dropping a per-request mutex lock and ring-buffer write from the AWS request hot path time: 2026-07-25T00:00:01.000000+09:00 custom: Issue: "112" diff --git a/changes/unreleased/Changed-20260730-000001.yaml b/changes/unreleased/Changed-20260730-000001.yaml index 1354dbd1..98a17c68 100644 --- a/changes/unreleased/Changed-20260730-000001.yaml +++ b/changes/unreleased/Changed-20260730-000001.yaml @@ -1,5 +1,5 @@ kind: Changed -body: The `services` config block is now optional. Omit it — as the embedded default now does — and every registered service starts with `data_dir ./data/`; listing services still restricts startup to exactly that set. This replaces a 325-line default file in which all 103 entries were identical boilerplate. +body: 'The `services` config block is now optional: omit it and every registered service starts with `data_dir ./data/`; list services and only those start. Replaces 325 lines of identical boilerplate.' time: 2026-07-30T00:00:01.000000+09:00 custom: Issue: "120" diff --git a/changes/unreleased/Fixed-20260718-222651.yaml b/changes/unreleased/Fixed-20260718-222651.yaml index 057f4793..802045ae 100644 --- a/changes/unreleased/Fixed-20260718-222651.yaml +++ b/changes/unreleased/Fixed-20260718-222651.yaml @@ -1,5 +1,5 @@ kind: Fixed -body: Correct `X-Amz-Target` service routing for AWS services whose target prefix is dotted (e.g. CodeConnections, DMS); they previously misrouted to CloudTrail via an overbroad `com` prefix match +body: Route `X-Amz-Target` prefixes that contain dots (CodeConnections, DMS) to the right service; they misrouted to CloudTrail time: 2026-07-18T22:26:51.484673+09:00 custom: Issue: "96" diff --git a/changes/unreleased/Fixed-20260718-222652.yaml b/changes/unreleased/Fixed-20260718-222652.yaml index 17b385e3..00e14168 100644 --- a/changes/unreleased/Fixed-20260718-222652.yaml +++ b/changes/unreleased/Fixed-20260718-222652.yaml @@ -1,5 +1,5 @@ kind: Fixed -body: Unimplemented operations now return a `NotImplemented` error (HTTP 501) instead of a misleading empty `200` success, so SDKs no longer see a false success and can tell a not-yet-emulated real operation apart from a genuinely invalid action; applied uniformly across all service providers (the ChangeResourceRecordSets record-action validation path keeps `InvalidChangeBatch`) +body: Unimplemented operations return `NotImplemented` (HTTP 501) instead of an empty `200`, so SDKs can tell a not-yet-emulated operation from an invalid one time: 2026-07-18T22:26:52.000000+09:00 custom: Issue: "96" diff --git a/changes/unreleased/Fixed-20260720-054448.yaml b/changes/unreleased/Fixed-20260720-054448.yaml index 44fc386f..a0523845 100644 --- a/changes/unreleased/Fixed-20260720-054448.yaml +++ b/changes/unreleased/Fixed-20260720-054448.yaml @@ -1,5 +1,5 @@ kind: Fixed -body: 'EC2: Describe{SecurityGroups,Tags,Addresses} now honor GroupId/AllocationId/PublicIp selectors and Filters (and raise NotFound for unknown IDs) instead of returning every resource; delete-by-name removes a single security group; batch Start/Stop is atomic; AssociateAddress validates the instance; duplicate/absent security-group rules are rejected; Authorize/Revoke accept GroupName; all-traffic rules omit ports' +body: 'EC2: Describe{SecurityGroups,Tags,Addresses} honor their selectors and Filters instead of returning every resource, unknown IDs raise NotFound, batch Start/Stop is atomic, and duplicate or absent security group rules are rejected' time: 2026-07-20T05:44:48.649862+09:00 custom: Issue: "97" diff --git a/changes/unreleased/Fixed-20260720-055500.yaml b/changes/unreleased/Fixed-20260720-055500.yaml index 229e6993..9603c17d 100644 --- a/changes/unreleased/Fixed-20260720-055500.yaml +++ b/changes/unreleased/Fixed-20260720-055500.yaml @@ -1,5 +1,5 @@ kind: Fixed -body: 'DynamoDB: numeric (''N'') sort/range keys now order by true numeric value across the full 38-digit precision via a NUMTEXT SQLite collation, instead of a float CAST that lost precision past 2^53' +body: 'DynamoDB: numeric sort keys order by true value over the full 38 digits via a NUMTEXT collation, instead of a float CAST that lost precision past 2^53' time: 2026-07-20T05:55:00.000000+09:00 custom: Issue: "97" diff --git a/changes/unreleased/Fixed-20260730-000002.yaml b/changes/unreleased/Fixed-20260730-000002.yaml index c28493ec..371d57a5 100644 --- a/changes/unreleased/Fixed-20260730-000002.yaml +++ b/changes/unreleased/Fixed-20260730-000002.yaml @@ -1,5 +1,5 @@ kind: Fixed -body: The weekly Smithy model sync could never report an update. `download-smithy-models.sh` skipped every model already in the tree (all of them), and its hand-maintained service list had drifted so that 14 entries 404'd upstream. The list is now derived from the models present, the workflow passes `--refresh` to re-download them, downloads are written atomically so a failed fetch can no longer delete a committed model, and the workflow's change check uses `git status --porcelain` so a newly generated package is no longer silently dropped as an untracked file. +body: 'The weekly Smithy model sync could never report an update: it skipped every model already in the tree (all of them), and 14 entries of its hand-maintained service list 404''d upstream. The list is now derived from the tree, the workflow re-downloads with `--refresh`, and downloads are atomic so a failed fetch cannot delete a committed model.' time: 2026-07-30T00:00:02.000000+09:00 custom: Issue: "120" diff --git a/changes/unreleased/Removed-20260724-000000.yaml b/changes/unreleased/Removed-20260724-000000.yaml index 0207a11c..e7030d9e 100644 --- a/changes/unreleased/Removed-20260724-000000.yaml +++ b/changes/unreleased/Removed-20260724-000000.yaml @@ -1,5 +1,5 @@ kind: Removed -body: Extract the Next.js web dashboard frontend to a separate repository. The Go binary no longer builds, bundles, or serves a web UI; it exposes only the opt-in admin API at `/devcloud/api/*`. +body: Extract the web dashboard to a separate repository. The binary builds and serves no UI, only the opt-in admin API at `/devcloud/api/*`. time: 2026-07-24T00:00:00.000000+09:00 custom: Issue: "111" diff --git a/changes/unreleased/Removed-20260730-000000.yaml b/changes/unreleased/Removed-20260730-000000.yaml index 3f6a786a..fe3a1925 100644 --- a/changes/unreleased/Removed-20260730-000000.yaml +++ b/changes/unreleased/Removed-20260730-000000.yaml @@ -1,5 +1,5 @@ kind: Removed -body: Remove dead surface found by a repo-wide over-engineering audit — the in-memory event bus and the admin WebSocket at `/devcloud/api/ws` (no code could ever publish an event, so the socket accepted connections and sent nothing), the `GetMetrics`/`ServiceMetrics` plugin API and the `/devcloud/api/metrics` endpoints (100 of 103 services returned zero-valued metrics; resource counts remain on `/devcloud/api/services`), the unused generic `shared.ResourceStore`, `gateway.ExtractAccountID` (it returned a constant and had no callers), and the `auth.enabled` key whose only effect was a startup warning that it was unimplemented (the key is still read for one release so that enabling it warns that credentials are accepted regardless, rather than being ignored in silence). Drops the `gorilla/websocket` dependency. +body: 'Remove dead surface found by a repo-wide over-engineering audit: the event bus and admin WebSocket, the `GetMetrics` plugin API and `/devcloud/api/metrics` (resource counts remain on `/devcloud/api/services`), `shared.ResourceStore`, `gateway.ExtractAccountID`, and the `auth.enabled` key — still read for one release so enabling it warns that credentials are accepted regardless. Drops the `gorilla/websocket` dependency.' time: 2026-07-30T00:00:00.000000+09:00 custom: Issue: "120"