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 f00ce6b..9ab31dc 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 0990b51..0ebd2ea 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 ea17310..3caceaf 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 30ebdc1..fd28320 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 3f1eda9..017cf05 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 25bb3d5..d80091c 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 72742ed..516c880 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 9226d0d..e099739 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 3cbbc18..4598387 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 5f89826..248c4fd 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 7c0d295..80b16bc 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 6df70a4..94c13f1 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 59d04d1..dfe2c84 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 907cc85..2c65a67 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 b45e9ce..3a7041e 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 4f24678..2bd4f22 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 2a7e002..0483fbe 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 4ff8a01..9a5c2e5 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 0e0e105..0ac5056 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 e7bc69e..8c194ad 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 3470d1e..cb6f330 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 9ac793c..738a6c6 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 efc7d51..ff46bf9 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 e5fbcda..0d69c79 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 e6e6782..72b65ec 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 4e49d02..993a2d2 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 d6886f5..3d2176c 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 f449674..b2eb4dd 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 b3b57a5..0e13990 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 11402d7..8c4863a 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 c224ca7..98b27de 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 8b8c174..3ebabb7 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 66aa744..1fc41e2 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 11cc8e9..6ffe7b8 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 55d4cdb..8d5f043 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 a41dfd4..516460c 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 6addebe..0ae3edc 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 f8d73ed..33c4f7f 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 9e5957c..461054c 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 d6d18cc..0debf61 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 4d626c8..a962ece 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 6de39f7..a5dd5df 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 ca17a9c..16be403 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 3b00cde..ad7a57a 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 45007a4..0dec954 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 b797997..b4631f2 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 ea3264a..77b6c4d 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 d2f26cf..798a595 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 6c86669..6688b85 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 d259b64..32e1108 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 53160fa..f052d06 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 3f8c73e..bbd5ca0 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 dc9532a..6cb56f9 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 1c06dcd..3d05c4a 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 4818e19..82a871e 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 8188874..feeb953 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 4f9d70f..2a1fbf7 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 7b1f244..c182ffa 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 98a068c..5313344 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 8c3600b..5b45d33 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 8bd9a15..13e3bfe 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 dcd9415..7b70149 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 20d671e..1cdaf57 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 f896f88..f1666e9 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 dbc9be3..0f11ca1 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 b43abf5..b42aec8 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 d45fc46..14a9b7e 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 55b0f5a..43790ae 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 b0f0f7f..72f4f27 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 2609a13..b5c9f51 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 e9fb3be..5b17ba7 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 130b875..6fa9773 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 1895c5c..a5b876e 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 0ea4545..1eaaf33 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 6514799..a4f7f57 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 281d178..b33f7e3 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 b503af6..740234c 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 e3a4aea..16b1b3a 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 e0a41e4..a2a4b13 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 0bdafeb..1427d5e 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 1f0f76c..f9fda23 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 64f22cb..a97f71c 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 34a680f..9321cf4 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 0657499..c2d2c82 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 678cab5..3631a83 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 3259b56..21c3103 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 8351e4f..d91a76a 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 a8a38b8..b8c6394 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 086ece1..6cde419 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 732d553..ee6ebd5 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 6055784..d654048 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 c8e9056..f14818c 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 55300aa..cc5cc88 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 bd9d115..d44fca5 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 183d567..22bfcdb 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 c763669..c1fb972 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 d3a3da3..9212e97 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 5e26fdc..4f55f6f 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 5535f6b..cb5efa7 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 4fc9438..436bc37 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 e55941b..1ff77f9 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 2d25625..123fe7c 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 4df65cd..10a5a83 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 b222d1d..5913dd1 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 c306f32..b1d9146 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 419190e..1d6ea56 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 9f75d31..1c18a94 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 29107df..7a3c4e2 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 d2cdc88..a15c6db 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 a4c700b..248d17c 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 7f1e7d1..c115dac 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 9fc40dd..07801af 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 18584f4..f54fd6e 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 afcbf40..2657495 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 e5d9ea5..4457c8e 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 4e7bc79..1abf243 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 081196d..ffdc96c 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 0000000..c77ffb2 --- /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 6c2a893..ee5fc00 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 3692fb7..0000000 --- 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 31b9d78..0000000 --- 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 2399c32..0000000 --- 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 ee5e036..0000000 --- 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 69dc908..73b3433 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 125e0ff..6284185 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 087a508..7844577 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 59c4a38..8ffa962 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 c494961..0000000 --- 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 a870b8a..0000000 --- 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 d096fd2..b04c983 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 880f742..2bddebb 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 2caf3c7..15fc1b1 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 9baa572..0000000 --- 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 62a4b6e..0000000 --- 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 31875cb..0000000 --- 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 48c256e..0000000 --- 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 593443d..60e1e32 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 b2dc733..4de28b6 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 2c560f6..b2aa4d8 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 711e6b7..66f5730 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 d10bdbe..6fcf4a5 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 ae12d9a..a6643fc 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 8acf0a4..0376f4d 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 a955f7f..36ebe9b 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 064030e..114709b 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 f35c6f6..05db2e6 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 5025708..da387d6 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 acc306b..5657267 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 d60abab..3714a1c 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 0000000..1354dbd --- /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 0000000..c28493e --- /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 0000000..5c96536 --- /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 3714a1c..4d773a8 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 5c96536..3f6a786 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 73b3433..0719dab 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 36ebe9b..5cddca2 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 b04c983..8a58fa8 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 2bddebb..93c2b0d 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 60e1e32..3d6a04e 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 4e18597..fd905f4 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 516c880..1560e69 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 e099739..41d5638 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 4598387..9fdb627 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 248c4fd..26626d1 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 80b16bc..ebf7b1c 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 2c65a67..d5b3114 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 9a5c2e5..adb9f43 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 1fc41e2..e4fdccd 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 16be403..81f4dfd 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 ad7a57a..41c18f4 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 b4631f2..5bc575c 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 b42aec8..3b359e8 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 5b17ba7..ef119b6 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 a5b876e..74f80b6 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 c1fb972..d2cca64 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 10a5a83..2aa93f7 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 5913dd1..fe18aad 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 c77ffb2..097d448 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 6fcf4a5..9dc6ca5 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 a852ff8..0e60ccb 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 d05c891..566d789 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 bc82f9d..16c55c5 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 a968c0a..da7e8cb 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 5aed721..247c268 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 43ff339..7d5967b 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 4e9aff5..501059c 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 1354dbd..98a17c6 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 057f479..802045a 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 17b385e..00e1416 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 44fc386..a052384 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 229e699..9603c17 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 c28493e..371d57a 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 0207a11..e7030d9 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 3f6a786..fe3a192 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"