diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index a462d8d9..589f730a 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "6.31.0" + version = "6.32.1" constraints = ">= 4.56.0" hashes = [ - "h1:1s5nzQMTgvB1AR2yUZcLz7hDM4PdEZZkKdEnBKaq8AY=", - "h1:8ylMF1VIXmf5Dc2Ew5BGOx+T1jblHB0Ik6nzxWxuu2Y=", - "h1:KYem6P5vF6BD+OpuZxrD2Vj668i54BS7+x4pgu0U6DY=", - "h1:M7p95xWoE3zWo1hx0aGg7GB262c7OizE+h6ISrzl1So=", - "h1:NPMzGvaqqewWvKA5ButStYnFWbgFIExWNKuuHZug1sE=", - "h1:PWZAzhvwJ7ccFn9c7EPuCrmGZHk+4w1cqeNA0DDGh/I=", - "h1:QSBCfLTHwJ4iMMFXEwGnc0hJ4pVZ94cenZYLXjnLJx8=", - "h1:bpfP3nGNyZEDeITqMfP/RSZ7aNsdskoK1aDj1t3xWQw=", - "h1:cptVA3yG3sJCAFVcTAtdoreGvvNUqskDYWkGSe/toB4=", - "h1:frOihN9Tg4JBTrN5eBhWmMcR4chDM+YJazvwgXQSHFU=", - "h1:mbLgY4xcgqOsFbkWMzOHa478thZM5SX1ci0F7Isf+7k=", - "h1:pQvUqZS7sRQzYj06cmDoRL/AkRgGo16laRl2nDQC0hQ=", - "h1:uOBPLestaUsogblJEzezIuPD3yi2VqAxjMn3usJ7i5g=", - "h1:yrswE/HFLKpPvs2622EyUPWv87ir0OuixxhrESWTt4g=", - "zh:0184b83f61dfb2f90f051d6a10e85d554809eb7dec13c49000bc884cfd1e956d", - "zh:16f76019ad67f0dfafea2c65b17bd1aa289cb5c275521df71337e23b08af6fec", - "zh:296ebaa261729b78159694e3ca709735c5c67913d6107c7e1abd4d1e9b05fc6b", - "zh:6b4c37bd7e8abca1b428903212de731b04695dcc59e2ba2acefc3d936b36c4dc", - "zh:6f49e2f7464dbb9d6911dd32951637f589d21a5c3c9a7c5056837701977ec803", - "zh:6fc4095e59286dd83e9528346390b0b07b3bffa1d46b50027c9e080352207626", - "zh:98816c0c5d1b956b564c2d2feb423fdf4eb3e476a6c5202668a285ff3b2d6910", + "h1:/kSj+4KeiYIJR4GZKUIp+NjaOSGPbEpoJFo+n8r21iQ=", + "h1:2vgwE6+ZCd7tLwQOb41OO0nLYAgV7ssIb8Xr9CdUupo=", + "h1:5tbI29RszzXinjHPzy5Qqp1ooS3/T+zLrjodyr0osJs=", + "h1:CAu73BoUtKnbgWP6oBs5pCTXL+Hfy8xc3sWZuZf1vEk=", + "h1:HYVnQr6ZXWVB/U3j/VuDZmn5fdzrCD6StyC3t7LM150=", + "h1:ONBRGZUR973/u9y3Yf2yxYGNHH4acyVm0r03iHxS4L8=", + "h1:Qhwre3rhX8AN+kAOiNjyS9uNtjlpK8yhwtoQPAJ2HyM=", + "h1:S9FhFACHbATLcz5I7dsM7KwUL94hdyfxj2AhHZ4rtKA=", + "h1:e8ls5XHmaRt5w4XVpWhon8BnmdaFqYtm+kIhIutyR4g=", + "h1:j691GxEePvwjhYV08mwgTLD/CiCG4YHdZOXL+gV6qt0=", + "h1:mdjVlnAux6Ddq2c+14yDmwT1PBK0t9D/v/y2sv3Pyu4=", + "h1:pIWlu/D0yFPM5dk37TWNaRUZZSNOs9MW7to8IP/F5TA=", + "h1:uHqBzBSaSuJrwsqcbF4kY5tW8hc2qAlVLAl7QdRrEmE=", + "h1:yqfHWALCdPdktH7iDa9gWKmxHeTl8r7Wu97JDeRO3tI=", + "zh:024d2cc116c8c83bb63b71623e3654109948791b250929449f4533b06678d574", + "zh:0ee944eb1c0b28957ad04541546ebac66f81b74ae811d20bcd7043d0313722e1", + "zh:43f1b6bcc2d6ba34dd4f02aab2ef3923281cf82455e608ac1ea493374dbb132d", + "zh:52e91c66c3d946d9d24ecf6684e23337abbe7e93a7e8d927f8b7cc69d096215e", + "zh:5d8030a02b61256fb6ee51efe70c1ddfc0d57b4dc0f25c621afddab81575a9c2", + "zh:67b25c8732af678af5772cf57bfb68937bdb535ef06f7f353202e272d843f52c", + "zh:6e846e85e55d7c49820410fb3db338e2d2adf19e3481558e3bec0d63b953c521", + "zh:8d4922a86a39cb2788c14f430008fcaf236b0023260439bc95cc7758d5b76f4a", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a70b34fb8d5a7d3b3823046938f3c9b0527afa93d02086b3d87ffa668c9a350e", - "zh:c24c0a58a8301d13cb4c27738840c8e7e0f29563ccf8d6b5ca1c87fcf21bdf89", - "zh:c95d44b2baea56b03198acaaf50f9196504d0207118e1afca7d70b5840315dc4", - "zh:db5a7692e2bde721a37b83f89eb9886715dbd17eb45858b1b58b8f7903ce5144", - "zh:db706b23a652e06c6c3f5de1da70e55a20a4fc2f73c01c24f7b9cd39a9a35f56", - "zh:fb781119fa98d8b0318ffb26ef013d5e674637e8f6a36b4b9c2742f24c022538", - "zh:fc459573b260a5a295d5fed442bf4f44fe034a0702ca22a65d320bd3b3e70eb5", + "zh:9e3d4d1848fc6675c6bd88087188f229c4ec98b1a35de97c2697a0160fb76678", + "zh:b21c1b932c896c21988baac3b1cbc8b51843581b8fabf5e396952a329c9e6a12", + "zh:df8e5b1a2713880e2b3c489cc22ad3b14490e1702a1637273f91747bf091c071", + "zh:ec66785d40f7c04f138bb94fec55b8ddaae6fcc9cb25cc388989150bfaf2de4c", + "zh:f1ecb00fcfdb0c2aec3622549c023f469db401f395bc25bbddfe5cf8b51cd046", + "zh:fca78bf28897c8077130ce8d0f4d67900dbd77619adb1326bcd017ef421e5f1f", ] } diff --git a/aws-source/adapters/adapterhelpers_always_get_source.go b/aws-source/adapters/adapterhelpers_always_get_source.go index 85c730ee..a946888a 100644 --- a/aws-source/adapters/adapterhelpers_always_get_source.go +++ b/aws-source/adapters/adapterhelpers_always_get_source.go @@ -9,9 +9,9 @@ import ( "buf.build/go/protovalidate" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/sourcegraph/conc/pool" ) diff --git a/aws-source/adapters/adapterhelpers_always_get_source_test.go b/aws-source/adapters/adapterhelpers_always_get_source_test.go index 66d1b10b..94bfd498 100644 --- a/aws-source/adapters/adapterhelpers_always_get_source_test.go +++ b/aws-source/adapters/adapterhelpers_always_get_source_test.go @@ -6,9 +6,9 @@ import ( "fmt" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_describe_source.go b/aws-source/adapters/adapterhelpers_describe_source.go index a5826ec6..e52bdc1f 100644 --- a/aws-source/adapters/adapterhelpers_describe_source.go +++ b/aws-source/adapters/adapterhelpers_describe_source.go @@ -8,9 +8,9 @@ import ( "time" "buf.build/go/protovalidate" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // relatively short cache duration to cover a single Change Analysis run. diff --git a/aws-source/adapters/adapterhelpers_describe_source_test.go b/aws-source/adapters/adapterhelpers_describe_source_test.go index 323a500c..50732af3 100644 --- a/aws-source/adapters/adapterhelpers_describe_source_test.go +++ b/aws-source/adapters/adapterhelpers_describe_source_test.go @@ -7,9 +7,9 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go b/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go index b156e921..05099a01 100644 --- a/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go +++ b/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go @@ -6,9 +6,9 @@ import ( "fmt" "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // GetListAdapterV2 A adapter for AWS APIs where the Get and List functions both diff --git a/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go b/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go index 6ca9a41f..57f81e07 100644 --- a/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go +++ b/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go @@ -6,9 +6,9 @@ import ( "fmt" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_get_list_source.go b/aws-source/adapters/adapterhelpers_get_list_source.go index abeb23d4..35b5a739 100644 --- a/aws-source/adapters/adapterhelpers_get_list_source.go +++ b/aws-source/adapters/adapterhelpers_get_list_source.go @@ -7,8 +7,8 @@ import ( "time" "buf.build/go/protovalidate" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // GetListAdapter A adapter for AWS APIs where the Get and List functions both diff --git a/aws-source/adapters/adapterhelpers_get_list_source_test.go b/aws-source/adapters/adapterhelpers_get_list_source_test.go index b0601b84..f6e01435 100644 --- a/aws-source/adapters/adapterhelpers_get_list_source_test.go +++ b/aws-source/adapters/adapterhelpers_get_list_source_test.go @@ -6,8 +6,8 @@ import ( "fmt" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_notfound_cache_test.go b/aws-source/adapters/adapterhelpers_notfound_cache_test.go index 3b17fc1d..25d67216 100644 --- a/aws-source/adapters/adapterhelpers_notfound_cache_test.go +++ b/aws-source/adapters/adapterhelpers_notfound_cache_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // TestGetListAdapterV2_GetNotFoundCaching tests that GetListAdapterV2 caches not-found error results diff --git a/aws-source/adapters/adapterhelpers_shared_tests.go b/aws-source/adapters/adapterhelpers_shared_tests.go index 5a690c5f..8bf6cda0 100644 --- a/aws-source/adapters/adapterhelpers_shared_tests.go +++ b/aws-source/adapters/adapterhelpers_shared_tests.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func PtrString(v string) *string { diff --git a/aws-source/adapters/adapterhelpers_util.go b/aws-source/adapters/adapterhelpers_util.go index 82c60cae..c77ac4e4 100644 --- a/aws-source/adapters/adapterhelpers_util.go +++ b/aws-source/adapters/adapterhelpers_util.go @@ -17,8 +17,8 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" awsHttp "github.com/aws/smithy-go/transport/http" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/aws-source/adapters/apigateway-api-key.go b/aws-source/adapters/apigateway-api-key.go index 8d055b73..2c4659b0 100644 --- a/aws-source/adapters/apigateway-api-key.go +++ b/aws-source/adapters/apigateway-api-key.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // convertGetApiKeyOutputToApiKey converts a GetApiKeyOutput to an ApiKey @@ -64,11 +64,6 @@ func apiKeyOutputMapper(scope string, awsItem *types.ApiKey) (*sdp.Item, error) Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate both ways - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/apigateway-api-key_test.go b/aws-source/adapters/apigateway-api-key_test.go index 21e4243b..1925e300 100644 --- a/aws-source/adapters/apigateway-api-key_test.go +++ b/aws-source/adapters/apigateway-api-key_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestApiKeyOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-authorizer.go b/aws-source/adapters/apigateway-authorizer.go index 0b4251f0..3653cce1 100644 --- a/aws-source/adapters/apigateway-authorizer.go +++ b/aws-source/adapters/apigateway-authorizer.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // convertGetAuthorizerOutputToAuthorizer converts a GetAuthorizerOutput to an Authorizer @@ -48,11 +48,6 @@ func authorizerOutputMapper(query, scope string, awsItem *types.Authorizer) (*sd Query: strings.Split(query, "/")[0], Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate the blast to the linked item - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-authorizer_test.go b/aws-source/adapters/apigateway-authorizer_test.go index e7ff174e..31830aaa 100644 --- a/aws-source/adapters/apigateway-authorizer_test.go +++ b/aws-source/adapters/apigateway-authorizer_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAuthorizerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-deployment.go b/aws-source/adapters/apigateway-deployment.go index c395d374..29ce47a2 100644 --- a/aws-source/adapters/apigateway-deployment.go +++ b/aws-source/adapters/apigateway-deployment.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // convertGetDeploymentOutputToDeployment converts a GetDeploymentOutput to a Deployment @@ -44,11 +44,6 @@ func deploymentOutputMapper(query, scope string, awsItem *types.Deployment) (*sd Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate the blast to the linked item - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -58,18 +53,6 @@ func deploymentOutputMapper(query, scope string, awsItem *types.Deployment) (*sd Query: fmt.Sprintf("%s/%s", restAPIID, *awsItem.Id), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - /* - If an aws_api_gateway_deployment is deleted, - any stage that references this deployment will be affected - because the stage will no longer have a valid deployment to point to. - However, if an aws_api_gateway_stage is deleted, - it does not affect the aws_api_gateway_deployment itself, - but it will remove the specific environment where the deployment was available. - */ - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-deployment_test.go b/aws-source/adapters/apigateway-deployment_test.go index 98ae4f60..0904e875 100644 --- a/aws-source/adapters/apigateway-deployment_test.go +++ b/aws-source/adapters/apigateway-deployment_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDeploymentOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-domain-name.go b/aws-source/adapters/apigateway-domain-name.go index 9b767355..97cc6c7a 100644 --- a/aws-source/adapters/apigateway-domain-name.go +++ b/aws-source/adapters/apigateway-domain-name.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func convertGetDomainNameOutputToDomainName(output *apigateway.GetDomainNameOutput) *types.DomainName { @@ -74,12 +74,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.RegionalHostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the domain name - In: true, - // The domain name won't affect the hosted zone - Out: false, - }, }) } @@ -92,12 +86,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.DistributionHostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the domain name - In: true, - // The domain name won't affect the hosted zone - Out: false, - }, }) } @@ -111,11 +99,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.CertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } } @@ -130,11 +113,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.RegionalCertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } } @@ -148,11 +126,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.RegionalDomainName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } @@ -166,11 +139,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.OwnershipVerificationCertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/apigateway-domain-name_test.go b/aws-source/adapters/apigateway-domain-name_test.go index 9a4f058d..80798499 100644 --- a/aws-source/adapters/apigateway-domain-name_test.go +++ b/aws-source/adapters/apigateway-domain-name_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-integration.go b/aws-source/adapters/apigateway-integration.go index bca58a04..a973f6c2 100644 --- a/aws-source/adapters/apigateway-integration.go +++ b/aws-source/adapters/apigateway-integration.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type apiGatewayIntegrationGetter interface { @@ -61,11 +61,6 @@ func apiGatewayIntegrationGetFunc(ctx context.Context, client apiGatewayIntegrat Query: fmt.Sprintf("%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) if output.ConnectionId != nil { @@ -76,12 +71,6 @@ func apiGatewayIntegrationGetFunc(ctx context.Context, client apiGatewayIntegrat Query: *output.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // If VPC link goes away, so does the integration - In: true, - // If integration goes away, VPC link is still there - Out: false, - }, }) } diff --git a/aws-source/adapters/apigateway-integration_test.go b/aws-source/adapters/apigateway-integration_test.go index efec09ee..e400680c 100644 --- a/aws-source/adapters/apigateway-integration_test.go +++ b/aws-source/adapters/apigateway-integration_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type mockAPIGatewayIntegrationClient struct{} diff --git a/aws-source/adapters/apigateway-method-response.go b/aws-source/adapters/apigateway-method-response.go index 7e88cc03..7c8f185b 100644 --- a/aws-source/adapters/apigateway-method-response.go +++ b/aws-source/adapters/apigateway-method-response.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func apiGatewayMethodResponseGetFunc(ctx context.Context, client apigatewayClient, scope string, input *apigateway.GetMethodResponseInput) (*sdp.Item, error) { @@ -56,11 +56,6 @@ func apiGatewayMethodResponseGetFunc(ctx context.Context, client apigatewayClien Query: fmt.Sprintf("%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) return item, nil diff --git a/aws-source/adapters/apigateway-method-response_test.go b/aws-source/adapters/apigateway-method-response_test.go index 6c351c4f..97699a12 100644 --- a/aws-source/adapters/apigateway-method-response_test.go +++ b/aws-source/adapters/apigateway-method-response_test.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (m *mockAPIGatewayClient) GetMethodResponse(ctx context.Context, params *apigateway.GetMethodResponseInput, optFns ...func(*apigateway.Options)) (*apigateway.GetMethodResponseOutput, error) { diff --git a/aws-source/adapters/apigateway-method.go b/aws-source/adapters/apigateway-method.go index 997af413..8c2a7658 100644 --- a/aws-source/adapters/apigateway-method.go +++ b/aws-source/adapters/apigateway-method.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type apigatewayClient interface { @@ -61,11 +61,6 @@ func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope Query: methodID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) } @@ -77,12 +72,6 @@ func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope Query: fmt.Sprintf("%s/%s", *input.RestApiId, *output.AuthorizerId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting authorizer will affect the method - In: true, - // Deleting method won't affect the authorizer - Out: false, - }, }) } @@ -94,12 +83,6 @@ func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope Query: fmt.Sprintf("%s/%s", *input.RestApiId, *output.RequestValidatorId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting request validator will affect the method - In: true, - // Deleting method won't affect the request validator - Out: false, - }, }) } @@ -112,11 +95,6 @@ func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope Query: fmt.Sprintf("%s/%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod, statusCode), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/apigateway-method_test.go b/aws-source/adapters/apigateway-method_test.go index 8e71e819..69b8b1f9 100644 --- a/aws-source/adapters/apigateway-method_test.go +++ b/aws-source/adapters/apigateway-method_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type mockAPIGatewayClient struct{} diff --git a/aws-source/adapters/apigateway-model.go b/aws-source/adapters/apigateway-model.go index 8abe16c6..ddb710a0 100644 --- a/aws-source/adapters/apigateway-model.go +++ b/aws-source/adapters/apigateway-model.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func convertGetModelOutputToModel(output *apigateway.GetModelOutput) *types.Model { @@ -49,11 +49,6 @@ func modelOutputMapper(query, scope string, awsItem *types.Model) (*sdp.Item, er Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate the blast to the linked item - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-model_test.go b/aws-source/adapters/apigateway-model_test.go index 33f2d343..28f539f9 100644 --- a/aws-source/adapters/apigateway-model_test.go +++ b/aws-source/adapters/apigateway-model_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestModelOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-resource.go b/aws-source/adapters/apigateway-resource.go index 0513fb7c..f4a453ce 100644 --- a/aws-source/adapters/apigateway-resource.go +++ b/aws-source/adapters/apigateway-resource.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func convertGetResourceOutputToResource(output *apigateway.GetResourceOutput) *types.Resource { @@ -65,10 +65,6 @@ func resourceOutputMapper(query, scope string, awsItem *types.Resource) (*sdp.It Query: fmt.Sprintf("%s/%s/%s", restApiID, *awsItem.Id, methodString), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/apigateway-resource_test.go b/aws-source/adapters/apigateway-resource_test.go index a6bac53c..94352d27 100644 --- a/aws-source/adapters/apigateway-resource_test.go +++ b/aws-source/adapters/apigateway-resource_test.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-rest-api.go b/aws-source/adapters/apigateway-rest-api.go index d0515aba..b7f8fe64 100644 --- a/aws-source/adapters/apigateway-rest-api.go +++ b/aws-source/adapters/apigateway-rest-api.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" ) @@ -98,12 +98,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: vpcEndpointID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Any change on the VPC endpoint should affect the REST API - In: true, - // We can't affect the VPC endpoint - Out: false, - }, }) } } @@ -116,11 +110,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: fmt.Sprintf("%s/%s", *awsItem.Id, *awsItem.RootResourceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } @@ -131,12 +120,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Updating a resource won't affect the REST API - In: false, - // Updating the REST API will affect the resources - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -146,11 +129,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -160,11 +138,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: false, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -174,11 +147,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -188,11 +156,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-rest-api_test.go b/aws-source/adapters/apigateway-rest-api_test.go index f53a0468..459fd95b 100644 --- a/aws-source/adapters/apigateway-rest-api_test.go +++ b/aws-source/adapters/apigateway-rest-api_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-stage.go b/aws-source/adapters/apigateway-stage.go index 12688dcf..52309923 100644 --- a/aws-source/adapters/apigateway-stage.go +++ b/aws-source/adapters/apigateway-stage.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func convertGetStageOutputToStage(output *apigateway.GetStageOutput) *types.Stage { @@ -65,12 +65,6 @@ func stageOutputMapper(query, scope string, awsItem *types.Stage) (*sdp.Item, er Query: fmt.Sprintf("%s/%s", restAPIID, *awsItem.DeploymentId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting a deployment will impact the stage - In: true, - // Deleting a stage won't impact the deployment - Out: false, - }, }) } @@ -81,11 +75,6 @@ func stageOutputMapper(query, scope string, awsItem *types.Stage) (*sdp.Item, er Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate the blast to the linked item - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-stage_test.go b/aws-source/adapters/apigateway-stage_test.go index c852377c..ad4bd098 100644 --- a/aws-source/adapters/apigateway-stage_test.go +++ b/aws-source/adapters/apigateway-stage_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestStageOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-group.go b/aws-source/adapters/autoscaling-auto-scaling-group.go index c291bfdd..9601ab14 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-group.go +++ b/aws-source/adapters/autoscaling-auto-scaling-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scope string, _ *autoscaling.DescribeAutoScalingGroupsInput, output *autoscaling.DescribeAutoScalingGroupsOutput) ([]*sdp.Item, error) { @@ -52,12 +52,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.LaunchTemplateId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a launch template will affect the ASG - In: true, - // Changes to an ASG won't affect the template - Out: false, - }, }) } } @@ -76,12 +70,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: tgARN, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a target group won't affect the ASG - In: false, - // Changes to an ASG will affect the target group - Out: true, - }, }) } } @@ -95,14 +83,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *instance.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to an instance could affect the ASG since it - // could cause it to scale - In: true, - // Changes to an ASG can definitely affect an instance - // since it might be terminated - Out: true, - }, }) } @@ -115,12 +95,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *instance.LaunchTemplate.LaunchTemplateId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a launch template will affect the ASG - In: true, - // Changes to an ASG won't affect the template - Out: false, - }, }) } } @@ -135,13 +109,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.ServiceLinkedRoleARN, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a role can affect the functioning of the - // ASG - In: true, - // ASG changes wont affect the role though - Out: false, - }, }) } } @@ -154,11 +121,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.LaunchConfigurationName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Very tightly coupled - In: true, - Out: true, - }, }) } @@ -171,12 +133,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.LaunchTemplate.LaunchTemplateId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a launch template will affect the ASG - In: true, - // Changes to an ASG won't affect the template - Out: false, - }, }) } } @@ -189,12 +145,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.PlacementGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a placement group can affect the ASG - In: true, - // Changes to an ASG can affect the placement group - Out: true, - }, }) } diff --git a/aws-source/adapters/autoscaling-auto-scaling-group_test.go b/aws-source/adapters/autoscaling-auto-scaling-group_test.go index ea3a4c79..fb14825b 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-group_test.go +++ b/aws-source/adapters/autoscaling-auto-scaling-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAutoScalingGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-policy.go b/aws-source/adapters/autoscaling-auto-scaling-policy.go index b6ce7685..5f60fadb 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-policy.go +++ b/aws-source/adapters/autoscaling-auto-scaling-policy.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func scalingPolicyOutputMapper(_ context.Context, _ *autoscaling.Client, scope string, _ *autoscaling.DescribePoliciesInput, output *autoscaling.DescribePoliciesOutput) ([]*sdp.Item, error) { @@ -51,13 +51,6 @@ func scalingPolicyOutputMapper(_ context.Context, _ *autoscaling.Client, scope s Query: *policy.AutoScalingGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the ASG can affect the policy (e.g., if the - // ASG is deleted, the policy is also deleted) - In: true, - // Changes to the policy can affect the ASG behavior - Out: true, - }, }) // Link to CloudWatch Alarms @@ -70,13 +63,6 @@ func scalingPolicyOutputMapper(_ context.Context, _ *autoscaling.Client, scope s Query: *alarm.AlarmName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Alarms trigger policies, so changes to alarms can - // affect policy execution - In: true, - // Changes to the policy don't affect the alarm itself - Out: false, - }, }) } } @@ -146,13 +132,6 @@ func parseResourceLabelLinks(resourceLabel string, scope string) []*sdp.LinkedIt Query: sections[1], Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the load balancer can affect the scaling policy - // (e.g., if the LB is deleted or modified) - In: true, - // The scaling policy doesn't directly affect the load balancer - Out: false, - }, }) } @@ -166,13 +145,6 @@ func parseResourceLabelLinks(resourceLabel string, scope string) []*sdp.LinkedIt Query: sections[i+1], Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the target group can affect the scaling policy - // (e.g., request count metrics come from the target group) - In: true, - // The scaling policy doesn't directly affect the target group - Out: false, - }, }) break } diff --git a/aws-source/adapters/autoscaling-auto-scaling-policy_test.go b/aws-source/adapters/autoscaling-auto-scaling-policy_test.go index 27762c28..70929e68 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-policy_test.go +++ b/aws-source/adapters/autoscaling-auto-scaling-policy_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestScalingPolicyOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-cache-policy.go b/aws-source/adapters/cloudfront-cache-policy.go index d6a972ed..68b9a8a2 100644 --- a/aws-source/adapters/cloudfront-cache-policy.go +++ b/aws-source/adapters/cloudfront-cache-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func cachePolicyListFunc(ctx context.Context, client CloudFrontClient, scope string) ([]*types.CachePolicy, error) { diff --git a/aws-source/adapters/cloudfront-cache-policy_test.go b/aws-source/adapters/cloudfront-cache-policy_test.go index db2b8cac..8380c496 100644 --- a/aws-source/adapters/cloudfront-cache-policy_test.go +++ b/aws-source/adapters/cloudfront-cache-policy_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var testCachePolicy = &types.CachePolicy{ diff --git a/aws-source/adapters/cloudfront-continuous-deployment-policy.go b/aws-source/adapters/cloudfront-continuous-deployment-policy.go index 3fe11481..15546a8b 100644 --- a/aws-source/adapters/cloudfront-continuous-deployment-policy.go +++ b/aws-source/adapters/cloudfront-continuous-deployment-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func continuousDeploymentPolicyItemMapper(_, scope string, awsItem *types.ContinuousDeploymentPolicy) (*sdp.Item, error) { @@ -33,11 +33,6 @@ func continuousDeploymentPolicyItemMapper(_, scope string, awsItem *types.Contin Query: name, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go b/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go index ad4f2576..77c38358 100644 --- a/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go +++ b/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestContinuousDeploymentPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-distribution.go b/aws-source/adapters/cloudfront-distribution.go index 78422672..fd7d1025 100644 --- a/aws-source/adapters/cloudfront-distribution.go +++ b/aws-source/adapters/cloudfront-distribution.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var s3DnsRegex = regexp.MustCompile(`([^\.]+)\.s3\.([^\.]+)\.amazonaws\.com`) @@ -72,11 +72,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *d.DomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always linked - In: true, - Out: true, - }, }) } @@ -90,12 +85,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *keyGroup.KeyGroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The distribution won't affect the key group - Out: false, - // The key group could affect the distribution - In: true, - }, }) } } @@ -110,11 +99,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *record.CNAME, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } @@ -129,11 +113,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: alias, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } @@ -146,11 +125,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.ContinuousDeploymentPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - Out: true, - In: true, - }, }) } @@ -164,12 +138,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.CachePolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -181,12 +149,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.FieldLevelEncryptionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the encryption will affect the distribution - In: true, - // The distribution won't affect the encryption - Out: false, - }, }) } @@ -198,12 +160,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.OriginRequestPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -216,12 +172,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.RealtimeLogConfigArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config will affect the distribution - In: true, - // The distribution won't affect the config - Out: false, - }, }) } } @@ -234,12 +184,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.ResponseHeadersPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -252,12 +196,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Method: sdp.QueryMethod_GET, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key group will affect the distribution - In: true, - // The distribution won't affect the key group - Out: false, - }, }) } } @@ -273,12 +211,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *function.FunctionARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the function could affect the distribution - In: true, - // The distribution could affect the function - Out: true, - }, }) } } @@ -295,12 +227,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *function.LambdaFunctionARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the function could affect the distribution - In: true, - // The distribution could affect the function - Out: true, - }, }) } } @@ -318,11 +244,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *origin.DomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } @@ -334,12 +255,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *origin.OriginAccessControlId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the access identity will affect the distribution - In: true, - // The distribution won't affect the access identity - Out: false, - }, }) } @@ -357,12 +272,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: matches[1], Scope: FormatScope(scope, ""), // S3 buckets are global }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the bucket could affect the distribution - In: true, - // The distribution could affect the bucket - Out: true, - }, }) } } @@ -375,12 +284,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *origin.S3OriginConfig.OriginAccessIdentity, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the access identity will affect the distribution - In: true, - // The distribution won't affect the access identity - Out: false, - }, }) } } @@ -396,12 +299,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.CachePolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -413,12 +310,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.FieldLevelEncryptionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the encryption will affect the distribution - In: true, - // The distribution won't affect the encryption - Out: false, - }, }) } @@ -430,12 +321,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.OriginRequestPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -448,12 +333,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.RealtimeLogConfigArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config will affect the distribution - In: true, - // The distribution won't affect the config - Out: false, - }, }) } } @@ -466,12 +345,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.ResponseHeadersPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -484,12 +357,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Method: sdp.QueryMethod_GET, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key group will affect the distribution - In: true, - // The distribution won't affect the key group - Out: false, - }, }) } } @@ -505,12 +372,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *function.FunctionARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the function could affect the distribution - In: true, - // The distribution could affect the function - Out: true, - }, }) } } @@ -527,12 +388,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *function.LambdaFunctionARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the function could affect the distribution - In: true, - // The distribution could affect the function - Out: true, - }, }) } } @@ -547,11 +402,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.Logging.Bucket, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } @@ -565,12 +415,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.ViewerCertificate.ACMCertificateArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the certificate could affect the distribution - In: true, - // The distribution could not affect the certificate - Out: false, - }, }) } } @@ -582,12 +426,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.ViewerCertificate.IAMCertificateId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the certificate could affect the distribution - In: true, - // The distribution could not affect the certificate - Out: false, - }, }) } } @@ -601,12 +439,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.WebACLId, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the ACL could affect the distribution - In: true, - // The distribution could not affect the ACL - Out: false, - }, }) } else { // Else assume it's a V1 ID @@ -617,12 +449,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.WebACLId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the ACL could affect the distribution - In: true, - // The distribution could not affect the ACL - Out: false, - }, }) } } diff --git a/aws-source/adapters/cloudfront-distribution_test.go b/aws-source/adapters/cloudfront-distribution_test.go index 4a35cab8..20c713df 100644 --- a/aws-source/adapters/cloudfront-distribution_test.go +++ b/aws-source/adapters/cloudfront-distribution_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloudfront.GetDistributionInput, optFns ...func(*cloudfront.Options)) (*cloudfront.GetDistributionOutput, error) { diff --git a/aws-source/adapters/cloudfront-function.go b/aws-source/adapters/cloudfront-function.go index 29dc5a82..ea4239cc 100644 --- a/aws-source/adapters/cloudfront-function.go +++ b/aws-source/adapters/cloudfront-function.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func functionItemMapper(_, scope string, awsItem *types.FunctionSummary) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-function_test.go b/aws-source/adapters/cloudfront-function_test.go index a1c903f0..83f3fe9b 100644 --- a/aws-source/adapters/cloudfront-function_test.go +++ b/aws-source/adapters/cloudfront-function_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestFunctionItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-key-group.go b/aws-source/adapters/cloudfront-key-group.go index 32a8d30b..2c80906f 100644 --- a/aws-source/adapters/cloudfront-key-group.go +++ b/aws-source/adapters/cloudfront-key-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func KeyGroupItemMapper(_, scope string, awsItem *types.KeyGroup) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-key-group_test.go b/aws-source/adapters/cloudfront-key-group_test.go index 2e9defe0..c874fae0 100644 --- a/aws-source/adapters/cloudfront-key-group_test.go +++ b/aws-source/adapters/cloudfront-key-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestKeyGroupItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-origin-access-control.go b/aws-source/adapters/cloudfront-origin-access-control.go index 36dfd9ad..582502e5 100644 --- a/aws-source/adapters/cloudfront-origin-access-control.go +++ b/aws-source/adapters/cloudfront-origin-access-control.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func originAccessControlListFunc(ctx context.Context, client *cloudfront.Client, scope string) ([]*types.OriginAccessControl, error) { diff --git a/aws-source/adapters/cloudfront-origin-access-control_test.go b/aws-source/adapters/cloudfront-origin-access-control_test.go index 52ce8711..25621cce 100644 --- a/aws-source/adapters/cloudfront-origin-access-control_test.go +++ b/aws-source/adapters/cloudfront-origin-access-control_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestOriginAccessControlItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-origin-request-policy.go b/aws-source/adapters/cloudfront-origin-request-policy.go index 00c3069f..e3f97e51 100644 --- a/aws-source/adapters/cloudfront-origin-request-policy.go +++ b/aws-source/adapters/cloudfront-origin-request-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func originRequestPolicyItemMapper(_, scope string, awsItem *types.OriginRequestPolicy) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-origin-request-policy_test.go b/aws-source/adapters/cloudfront-origin-request-policy_test.go index fb00556d..4458f998 100644 --- a/aws-source/adapters/cloudfront-origin-request-policy_test.go +++ b/aws-source/adapters/cloudfront-origin-request-policy_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestOriginRequestPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-realtime-log-config.go b/aws-source/adapters/cloudfront-realtime-log-config.go index ac2f259b..be53fd7a 100644 --- a/aws-source/adapters/cloudfront-realtime-log-config.go +++ b/aws-source/adapters/cloudfront-realtime-log-config.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func realtimeLogConfigsItemMapper(_, scope string, awsItem *types.RealtimeLogConfig) (*sdp.Item, error) { @@ -35,12 +35,6 @@ func realtimeLogConfigsItemMapper(_, scope string, awsItem *types.RealtimeLogCon Query: *endpoint.KinesisStreamConfig.RoleARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the role will affect us - In: true, - // We can't affect the role - Out: false, - }, }) } } @@ -54,12 +48,6 @@ func realtimeLogConfigsItemMapper(_, scope string, awsItem *types.RealtimeLogCon Query: *endpoint.KinesisStreamConfig.StreamARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to this will affect the stream - Out: true, - // The stream can affect us - In: true, - }, }) } } diff --git a/aws-source/adapters/cloudfront-realtime-log-config_test.go b/aws-source/adapters/cloudfront-realtime-log-config_test.go index 23ebbe9e..58ae1589 100644 --- a/aws-source/adapters/cloudfront-realtime-log-config_test.go +++ b/aws-source/adapters/cloudfront-realtime-log-config_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestRealtimeLogConfigsItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-response-headers-policy.go b/aws-source/adapters/cloudfront-response-headers-policy.go index 6a4d80e9..4ff4cab0 100644 --- a/aws-source/adapters/cloudfront-response-headers-policy.go +++ b/aws-source/adapters/cloudfront-response-headers-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func ResponseHeadersPolicyItemMapper(_, scope string, awsItem *types.ResponseHeadersPolicy) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-response-headers-policy_test.go b/aws-source/adapters/cloudfront-response-headers-policy_test.go index 9557d875..e188964a 100644 --- a/aws-source/adapters/cloudfront-response-headers-policy_test.go +++ b/aws-source/adapters/cloudfront-response-headers-policy_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestResponseHeadersPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-streaming-distribution.go b/aws-source/adapters/cloudfront-streaming-distribution.go index 4ea0525e..024ba933 100644 --- a/aws-source/adapters/cloudfront-streaming-distribution.go +++ b/aws-source/adapters/cloudfront-streaming-distribution.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, scope string, input *cloudfront.GetStreamingDistributionInput) (*sdp.Item, error) { @@ -74,11 +74,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: *d.DomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always linked - In: true, - Out: true, - }, }) } @@ -92,11 +87,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: *dc.S3Origin.DomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } @@ -108,12 +98,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: *dc.S3Origin.OriginAccessIdentity, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the access identity will affect the distribution - In: true, - // The distribution won't affect the access identity - Out: false, - }, }) } } @@ -127,11 +111,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: alias, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } @@ -144,11 +123,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: *dc.Logging.Bucket, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/cloudfront-streaming-distribution_test.go b/aws-source/adapters/cloudfront-streaming-distribution_test.go index 6ae8ff7c..eaeb0502 100644 --- a/aws-source/adapters/cloudfront-streaming-distribution_test.go +++ b/aws-source/adapters/cloudfront-streaming-distribution_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t TestCloudFrontClient) GetStreamingDistribution(ctx context.Context, params *cloudfront.GetStreamingDistributionInput, optFns ...func(*cloudfront.Options)) (*cloudfront.GetStreamingDistributionOutput, error) { diff --git a/aws-source/adapters/cloudwatch-alarm.go b/aws-source/adapters/cloudwatch-alarm.go index 05353876..1d906def 100644 --- a/aws-source/adapters/cloudwatch-alarm.go +++ b/aws-source/adapters/cloudwatch-alarm.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type CloudwatchClient interface { @@ -154,12 +154,6 @@ func alarmOutputMapper(ctx context.Context, client CloudwatchClient, scope strin Query: arn.ResourceID(), Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the suppressor alarm will affect this alarm - In: true, - // Changes to this alarm won't affect the suppressor alarm - Out: false, - }, }) } } @@ -308,12 +302,6 @@ func actionToLink(action string) (*sdp.LinkedItemQuery, error) { Query: action, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the policy won't affect the alarm - In: false, - // Changes to the metric alarm will affect the policy - Out: true, - }, }, nil case "sns": return &sdp.LinkedItemQuery{ @@ -323,12 +311,6 @@ func actionToLink(action string) (*sdp.LinkedItemQuery, error) { Query: action, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the topic won't affect the alarm - In: false, - // Changes to the alarm will affect the topic - Out: true, - }, }, nil case "ssm": return &sdp.LinkedItemQuery{ @@ -338,12 +320,6 @@ func actionToLink(action string) (*sdp.LinkedItemQuery, error) { Query: action, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to an ops item won't affect the alarm - In: false, - // Changes to the alarm will affect the ops item - Out: true, - }, }, nil case "ssm-incidents": return &sdp.LinkedItemQuery{ @@ -353,12 +329,6 @@ func actionToLink(action string) (*sdp.LinkedItemQuery, error) { Query: action, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a response plan won't affect the alarm - In: false, - // Changes to the alarm will affect the response plan - Out: true, - }, }, nil default: return nil, errors.New("unknown service in ARN: " + arn.Service) diff --git a/aws-source/adapters/cloudwatch-alarm_test.go b/aws-source/adapters/cloudwatch-alarm_test.go index 929e2269..be179f7e 100644 --- a/aws-source/adapters/cloudwatch-alarm_test.go +++ b/aws-source/adapters/cloudwatch-alarm_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type testCloudwatchClient struct{} diff --git a/aws-source/adapters/cloudwatch-instance-metric.go b/aws-source/adapters/cloudwatch-instance-metric.go index 9823da80..ef9a0e6a 100644 --- a/aws-source/adapters/cloudwatch-instance-metric.go +++ b/aws-source/adapters/cloudwatch-instance-metric.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // CloudwatchMetricClient defines the CloudWatch client interface for metrics diff --git a/aws-source/adapters/cloudwatch-instance-metric_integration_test.go b/aws-source/adapters/cloudwatch-instance-metric_integration_test.go index 24f6598b..51208020 100644 --- a/aws-source/adapters/cloudwatch-instance-metric_integration_test.go +++ b/aws-source/adapters/cloudwatch-instance-metric_integration_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) // TestCloudwatchInstanceMetricIntegration fetches real CloudWatch metrics for an EC2 instance diff --git a/aws-source/adapters/cloudwatch-instance-metric_test.go b/aws-source/adapters/cloudwatch-instance-metric_test.go index e3aefc1f..ea7169c2 100644 --- a/aws-source/adapters/cloudwatch-instance-metric_test.go +++ b/aws-source/adapters/cloudwatch-instance-metric_test.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) // testCloudwatchMetricClient is a mock client for testing GetMetricData diff --git a/aws-source/adapters/cloudwatch_metric_links.go b/aws-source/adapters/cloudwatch_metric_links.go index edc8b3d2..b3816024 100644 --- a/aws-source/adapters/cloudwatch_metric_links.go +++ b/aws-source/adapters/cloudwatch_metric_links.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) var ErrNoQuery = errors.New("no query found") @@ -24,14 +24,6 @@ func SuggestedQuery(namespace string, scope string, dimensions []types.Dimension var query *sdp.Query var err error - bp := &sdp.BlastPropagation{ - // These links are the metrics that feed the alarms. If the thing that - // we're measuring changes, we definitely want the alarm to be in the - // blast radius. But an alarm on its own doesn't affect these things - In: false, - Out: true, - } - accountID, _, err := ParseScope(scope) if err != nil { @@ -244,7 +236,6 @@ func SuggestedQuery(namespace string, scope string, dimensions []types.Dimension return &sdp.LinkedItemQuery{ Query: query, - BlastPropagation: bp, }, err } diff --git a/aws-source/adapters/directconnect-connection.go b/aws-source/adapters/directconnect-connection.go index 19f28b3d..73fd78dc 100644 --- a/aws-source/adapters/directconnect-connection.go +++ b/aws-source/adapters/directconnect-connection.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeConnectionsInput, output *directconnect.DescribeConnectionsOutput) ([]*sdp.Item, error) { @@ -34,12 +34,6 @@ func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Cli Query: *connection.LagId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Connection and LAG are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -51,12 +45,6 @@ func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Cli Query: *connection.Location, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the location will affect this, i.e., its speed, provider, etc. - In: true, - // We can't affect the location - Out: false, - }, }) } @@ -68,12 +56,6 @@ func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Cli Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the loa will affect this - In: true, - // We can't affect the loa - Out: false, - }, }) } @@ -85,12 +67,6 @@ func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Cli Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the virtual interface won't affect this - In: false, - // We cannot delete a connection if it has virtual interfaces - Out: true, - }, }) items = append(items, &item) diff --git a/aws-source/adapters/directconnect-connection_test.go b/aws-source/adapters/directconnect-connection_test.go index c4b5e264..86dc2d8a 100644 --- a/aws-source/adapters/directconnect-connection_test.go +++ b/aws-source/adapters/directconnect-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectconnectConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-customer-metadata.go b/aws-source/adapters/directconnect-customer-metadata.go index 6f50dfe2..187903c9 100644 --- a/aws-source/adapters/directconnect-customer-metadata.go +++ b/aws-source/adapters/directconnect-customer-metadata.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func customerMetadataOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeCustomerMetadataInput, output *directconnect.DescribeCustomerMetadataOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-customer-metadata_test.go b/aws-source/adapters/directconnect-customer-metadata_test.go index 491a2b13..f2375df6 100644 --- a/aws-source/adapters/directconnect-customer-metadata_test.go +++ b/aws-source/adapters/directconnect-customer-metadata_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestCustomerMetadataOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go index f4a77d43..8fdfc4cd 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func directConnectGatewayAssociationProposalOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewayAssociationProposalsInput, output *directconnect.DescribeDirectConnectGatewayAssociationProposalsOutput) ([]*sdp.Item, error) { @@ -34,13 +34,6 @@ func directConnectGatewayAssociationProposalOutputMapper(_ context.Context, _ *d Query: fmt.Sprintf("%s/%s", *associationProposal.DirectConnectGatewayId, *associationProposal.AssociatedGateway.Id), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Any change on the association won't have an impact on the proposal - // Its life cycle ends when the association is accepted or rejected - In: true, - // Accepting a proposal will establish the association - Out: true, - }, }) } diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go index 5c836b06..942eff81 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectConnectGatewayAssociationProposalOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association.go b/aws-source/adapters/directconnect-direct-connect-gateway-association.go index 3f3f5a9c..4ac0a0f9 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) const ( @@ -47,12 +47,6 @@ func directConnectGatewayAssociationOutputMapper(_ context.Context, _ *directcon Query: *association.DirectConnectGatewayId, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting a direct connect gateway will change the state of the association - In: true, - // We can't affect the direct connect gateway - Out: false, - }, }) } @@ -64,12 +58,6 @@ func directConnectGatewayAssociationOutputMapper(_ context.Context, _ *directcon Query: *association.VirtualGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting a virtual gateway will change the state of the association - In: true, - // We can't affect the virtual gateway - Out: false, - }, }) } diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go index f46e8b0c..7fc04408 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectConnectGatewayAssociationOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go b/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go index 81ac79d9..aa2b3fa8 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func directConnectGatewayAttachmentOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewayAttachmentsInput, output *directconnect.DescribeDirectConnectGatewayAttachmentsOutput) ([]*sdp.Item, error) { @@ -50,12 +50,6 @@ func directConnectGatewayAttachmentOutputMapper(_ context.Context, _ *directconn Query: *attachment.DirectConnectGatewayId, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // This is not clearly documented, but it seems that if the gateway is deleted, the attachment state will change to detaching - In: true, - // We can't affect the direct connect gateway - Out: false, - }, }) } @@ -67,13 +61,6 @@ func directConnectGatewayAttachmentOutputMapper(_ context.Context, _ *directconn Query: *attachment.VirtualInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // If virtual interface is deleted, the attachment state will change to detaching - // https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DirectConnectGatewayAttachment.html#API_DirectConnectGatewayAttachment_Contents - In: true, - // We can't affect the virtual interface - Out: false, - }, }) } diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go index 1b8ad52d..096629a3 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectConnectGatewayAttachmentOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway.go b/aws-source/adapters/directconnect-direct-connect-gateway.go index 60c7b462..1d73ea81 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func directConnectGatewayOutputMapper(ctx context.Context, cli *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewaysInput, output *directconnect.DescribeDirectConnectGatewaysOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway_test.go b/aws-source/adapters/directconnect-direct-connect-gateway_test.go index c70f9f36..32ed2630 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectConnectGatewayOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-hosted-connection.go b/aws-source/adapters/directconnect-hosted-connection.go index 66d3baa4..cc6d552b 100644 --- a/aws-source/adapters/directconnect-hosted-connection.go +++ b/aws-source/adapters/directconnect-hosted-connection.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeHostedConnectionsInput, output *directconnect.DescribeHostedConnectionsOutput) ([]*sdp.Item, error) { @@ -34,12 +34,6 @@ func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *connection.LagId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Connection and LAG are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -51,12 +45,6 @@ func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *connection.Location, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the location will affect this, i.e., its speed, provider, etc. - In: true, - // We can't affect the location - Out: false, - }, }) } @@ -68,12 +56,6 @@ func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the loa will affect this - In: true, - // We can't affect the loa - Out: false, - }, }) } @@ -84,12 +66,6 @@ func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the virtual interface won't affect this - In: false, - // We cannot delete a hosted connection if it has virtual interfaces - Out: true, - }, }) items = append(items, &item) diff --git a/aws-source/adapters/directconnect-hosted-connection_test.go b/aws-source/adapters/directconnect-hosted-connection_test.go index 1ef0f8e2..6e869b45 100644 --- a/aws-source/adapters/directconnect-hosted-connection_test.go +++ b/aws-source/adapters/directconnect-hosted-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestHostedConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-interconnect.go b/aws-source/adapters/directconnect-interconnect.go index 5d0713d0..aef9d008 100644 --- a/aws-source/adapters/directconnect-interconnect.go +++ b/aws-source/adapters/directconnect-interconnect.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeInterconnectsInput, output *directconnect.DescribeInterconnectsOutput) ([]*sdp.Item, error) { @@ -53,12 +53,6 @@ func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope Query: *interconnect.InterconnectId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Interconnect and hosted connections are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -70,12 +64,6 @@ func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope Query: *interconnect.LagId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Interconnect and LAG are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -87,12 +75,6 @@ func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope Query: *interconnect.InterconnectId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the loa will affect this - In: true, - // We can't affect the loa - Out: false, - }, }) } @@ -104,12 +86,6 @@ func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope Query: *interconnect.Location, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the location will affect this, i.e., its speed, provider, etc. - In: true, - // We can't affect the location - Out: false, - }, }) } diff --git a/aws-source/adapters/directconnect-interconnect_test.go b/aws-source/adapters/directconnect-interconnect_test.go index 108f15d5..84f09a63 100644 --- a/aws-source/adapters/directconnect-interconnect_test.go +++ b/aws-source/adapters/directconnect-interconnect_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInterconnectOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-lag.go b/aws-source/adapters/directconnect-lag.go index 35e4c330..e18044ae 100644 --- a/aws-source/adapters/directconnect-lag.go +++ b/aws-source/adapters/directconnect-lag.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeLagsInput, output *directconnect.DescribeLagsOutput) ([]*sdp.Item, error) { @@ -54,12 +54,6 @@ func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Connection and LAG are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } } @@ -72,12 +66,6 @@ func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ Query: *lag.LagId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // LAG and hosted connections are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -90,12 +78,6 @@ func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ Query: *lag.Location, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the location will affect this, i.e., its speed, provider, etc. - In: true, - // We can't affect the location - Out: false, - }, }) } diff --git a/aws-source/adapters/directconnect-lag_test.go b/aws-source/adapters/directconnect-lag_test.go index 95b0815c..6632482b 100644 --- a/aws-source/adapters/directconnect-lag_test.go +++ b/aws-source/adapters/directconnect-lag_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" diff --git a/aws-source/adapters/directconnect-location.go b/aws-source/adapters/directconnect-location.go index 4d95c95b..f514f884 100644 --- a/aws-source/adapters/directconnect-location.go +++ b/aws-source/adapters/directconnect-location.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func locationOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeLocationsInput, output *directconnect.DescribeLocationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-location_test.go b/aws-source/adapters/directconnect-location_test.go index bd36c612..9c18c9f6 100644 --- a/aws-source/adapters/directconnect-location_test.go +++ b/aws-source/adapters/directconnect-location_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLocationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-router-configuration.go b/aws-source/adapters/directconnect-router-configuration.go index 70c1fc98..60d904b8 100644 --- a/aws-source/adapters/directconnect-router-configuration.go +++ b/aws-source/adapters/directconnect-router-configuration.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func routerConfigurationOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeRouterConfigurationInput, output *directconnect.DescribeRouterConfigurationOutput) ([]*sdp.Item, error) { @@ -34,11 +34,6 @@ func routerConfigurationOutputMapper(_ context.Context, _ *directconnect.Client, Query: *output.VirtualInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/directconnect-router-configuration_test.go b/aws-source/adapters/directconnect-router-configuration_test.go index 8a124b6e..6a151e2d 100644 --- a/aws-source/adapters/directconnect-router-configuration_test.go +++ b/aws-source/adapters/directconnect-router-configuration_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestRouterConfigurationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-virtual-gateway.go b/aws-source/adapters/directconnect-virtual-gateway.go index 53db1e36..36ed2c06 100644 --- a/aws-source/adapters/directconnect-virtual-gateway.go +++ b/aws-source/adapters/directconnect-virtual-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func virtualGatewayOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeVirtualGatewaysInput, output *directconnect.DescribeVirtualGatewaysOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-virtual-gateway_test.go b/aws-source/adapters/directconnect-virtual-gateway_test.go index 2f6252b9..c4003bb5 100644 --- a/aws-source/adapters/directconnect-virtual-gateway_test.go +++ b/aws-source/adapters/directconnect-virtual-gateway_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVirtualGatewayOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-virtual-interface.go b/aws-source/adapters/directconnect-virtual-interface.go index cda44169..aa57efc4 100644 --- a/aws-source/adapters/directconnect-virtual-interface.go +++ b/aws-source/adapters/directconnect-virtual-interface.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) const gatewayIDVirtualInterfaceIDFormat = "gateway_id/virtual_interface_id" @@ -37,12 +37,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We cannot delete a connection if it has virtual interfaces - In: true, - // We can't affect the connection - Out: false, - }, }) } @@ -54,12 +48,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.DirectConnectGatewayId, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // We cannot delete a direct connect gateway if it has virtual interfaces - In: true, - // We can't affect the direct connect gateway - Out: false, - }, }) } @@ -71,11 +59,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.AmazonAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // do not link through rdap definitions to avoid huge blast radius - In: false, - Out: false, - }, }) } @@ -87,11 +70,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.CustomerAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // do not link through rdap definitions to avoid huge blast radius - In: false, - Out: false, - }, }) } @@ -105,13 +83,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: fmt.Sprintf("%s/%s", *virtualInterface.DirectConnectGatewayId, *virtualInterface.VirtualInterfaceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the attachment won't affect virtual interface - In: false, - // If virtual interface is deleted, the attachment state will change to detaching - // https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DirectConnectGatewayAttachment.html#API_DirectConnectGatewayAttachment_Contents - Out: true, - }, }) } @@ -125,13 +96,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.VirtualInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the attachment won't affect virtual interface - In: false, - // If virtual interface is deleted, the attachment state will change to detaching - // https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DirectConnectGatewayAttachment.html#API_DirectConnectGatewayAttachment_Contents - Out: true, - }, }) } diff --git a/aws-source/adapters/directconnect-virtual-interface_test.go b/aws-source/adapters/directconnect-virtual-interface_test.go index 6a5463e8..396737b2 100644 --- a/aws-source/adapters/directconnect-virtual-interface_test.go +++ b/aws-source/adapters/directconnect-virtual-interface_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVirtualInterfaceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/dynamodb-backup.go b/aws-source/adapters/dynamodb-backup.go index cf67c84d..79905bdb 100644 --- a/aws-source/adapters/dynamodb-backup.go +++ b/aws-source/adapters/dynamodb-backup.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func backupGetFunc(ctx context.Context, client Client, scope string, input *dynamodb.DescribeBackupInput) (*sdp.Item, error) { @@ -55,14 +55,6 @@ func backupGetFunc(ctx context.Context, client Client, scope string, input *dyna Query: *out.BackupDescription.SourceTableDetails.TableName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the table could probably affect the backup - In: true, - // Changing the backup won't exactly affect the table in - // that it won't break it. But it could mean that it's no - // longer backed up so, blast propagation should be here too - Out: true, - }, }) } } diff --git a/aws-source/adapters/dynamodb-backup_test.go b/aws-source/adapters/dynamodb-backup_test.go index e4f4b718..4b924a28 100644 --- a/aws-source/adapters/dynamodb-backup_test.go +++ b/aws-source/adapters/dynamodb-backup_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *DynamoDBTestClient) DescribeBackup(ctx context.Context, params *dynamodb.DescribeBackupInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DescribeBackupOutput, error) { diff --git a/aws-source/adapters/dynamodb-table.go b/aws-source/adapters/dynamodb-table.go index 8aa3c8ad..5e4e06c0 100644 --- a/aws-source/adapters/dynamodb-table.go +++ b/aws-source/adapters/dynamodb-table.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func tableGetFunc(ctx context.Context, client Client, scope string, input *dynamodb.DescribeTableInput) (*sdp.Item, error) { @@ -83,14 +83,6 @@ func tableGetFunc(ctx context.Context, client Client, scope string, input *dynam Query: *dest.StreamArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // If you change the stream, it could mean the table - // is no longer replicated - In: true, - // Changing this table will affect the stream and - // whatever is listening to it - Out: true, - }, }) } } @@ -107,14 +99,6 @@ func tableGetFunc(ctx context.Context, client Client, scope string, input *dynam Query: *table.RestoreSummary.SourceBackupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The backup is just the source from which the table - // was created, so I guess you'd say that the recovery - // point affects the table - In: true, - // Changing the table won't affect the recovery point - Out: false, - }, }) } } @@ -128,14 +112,6 @@ func tableGetFunc(ctx context.Context, client Client, scope string, input *dynam Query: *table.RestoreSummary.SourceTableArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // If the table was restored from another table, and - // this is normal, then changing the source table could - // affect this one - In: true, - // Changing this table won't affect the source table - Out: false, - }, }) } } @@ -151,12 +127,6 @@ func tableGetFunc(ctx context.Context, client Client, scope string, input *dynam Query: *table.SSEDescription.KMSMasterKeyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key could affect the table - In: true, - // Changing the table won't affect the key - Out: false, - }, }) } } diff --git a/aws-source/adapters/dynamodb-table_test.go b/aws-source/adapters/dynamodb-table_test.go index 13589e01..ecbbfadd 100644 --- a/aws-source/adapters/dynamodb-table_test.go +++ b/aws-source/adapters/dynamodb-table_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *DynamoDBTestClient) DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) { diff --git a/aws-source/adapters/ec2-address.go b/aws-source/adapters/ec2-address.go index 999fd4a5..d96b444e 100644 --- a/aws-source/adapters/ec2-address.go +++ b/aws-source/adapters/ec2-address.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // AddressInputMapperGet Maps adapter calls to the correct input for the AZ API @@ -34,14 +34,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. var err error var attrs *sdp.ItemAttributes - // An EC2-address, along with an IP is an item that inherently links things - // and therefore should propagate blast radius in both directions on all - // links - bp := &sdp.BlastPropagation{ - In: true, - Out: true, - } - for _, address := range output.Addresses { attrs, err = ToAttributesWithExclude(address, "tags") @@ -62,7 +54,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.PublicIp, Scope: "global", }, - BlastPropagation: bp, }, }, Tags: ec2TagsToMap(address.Tags), @@ -76,7 +67,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.InstanceId, Scope: scope, }, - BlastPropagation: bp, }) } @@ -88,7 +78,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.CarrierIp, Scope: "global", }, - BlastPropagation: bp, }) } @@ -100,7 +89,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.CustomerOwnedIp, Scope: "global", }, - BlastPropagation: bp, }) } @@ -112,7 +100,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: bp, }) } @@ -124,7 +111,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.PrivateIpAddress, Scope: "global", }, - BlastPropagation: bp, }) } diff --git a/aws-source/adapters/ec2-address_test.go b/aws-source/adapters/ec2-address_test.go index 3132a84f..59262cc9 100644 --- a/aws-source/adapters/ec2-address_test.go +++ b/aws-source/adapters/ec2-address_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAddressInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-capacity-reservation-fleet.go b/aws-source/adapters/ec2-capacity-reservation-fleet.go index d07f2819..aa33087f 100644 --- a/aws-source/adapters/ec2-capacity-reservation-fleet.go +++ b/aws-source/adapters/ec2-capacity-reservation-fleet.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func capacityReservationFleetOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeCapacityReservationFleetsInput, output *ec2.DescribeCapacityReservationFleetsOutput) ([]*sdp.Item, error) { @@ -37,12 +37,6 @@ func capacityReservationFleetOutputMapper(_ context.Context, _ *ec2.Client, scop Query: *spec.CapacityReservationId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the fleet will affect the reservation - Out: true, - // The reservation won't affect us - In: false, - }, }) } } diff --git a/aws-source/adapters/ec2-capacity-reservation-fleet_test.go b/aws-source/adapters/ec2-capacity-reservation-fleet_test.go index 48bf1419..cb81a86f 100644 --- a/aws-source/adapters/ec2-capacity-reservation-fleet_test.go +++ b/aws-source/adapters/ec2-capacity-reservation-fleet_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestCapacityReservationFleetOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-capacity-reservation.go b/aws-source/adapters/ec2-capacity-reservation.go index 66188745..2034c63a 100644 --- a/aws-source/adapters/ec2-capacity-reservation.go +++ b/aws-source/adapters/ec2-capacity-reservation.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeCapacityReservationsInput, output *ec2.DescribeCapacityReservationsOutput) ([]*sdp.Item, error) { @@ -35,12 +35,6 @@ func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: *cr.CapacityReservationFleetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the fleet will affect this - In: true, - // We can't affect the fleet - Out: false, - }, }) } @@ -53,12 +47,6 @@ func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: *cr.OutpostArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the outpost will affect this - In: true, - // We can't affect the outpost - Out: false, - }, }) } } @@ -72,12 +60,6 @@ func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: *cr.PlacementGroupArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the placement group will affect this - In: true, - // We can't affect the placement group - Out: false, - }, }) } } diff --git a/aws-source/adapters/ec2-capacity-reservation_test.go b/aws-source/adapters/ec2-capacity-reservation_test.go index e2819dc4..94394fec 100644 --- a/aws-source/adapters/ec2-capacity-reservation_test.go +++ b/aws-source/adapters/ec2-capacity-reservation_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestCapacityReservationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-egress-only-internet-gateway.go b/aws-source/adapters/ec2-egress-only-internet-gateway.go index c1eec343..0fc882b9 100644 --- a/aws-source/adapters/ec2-egress-only-internet-gateway.go +++ b/aws-source/adapters/ec2-egress-only-internet-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func egressOnlyInternetGatewayInputMapperGet(scope string, query string) (*ec2.DescribeEgressOnlyInternetGatewaysInput, error) { @@ -54,12 +54,6 @@ func egressOnlyInternetGatewayOutputMapper(_ context.Context, _ *ec2.Client, sco Query: *attachment.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC won't affect the gateway - In: false, - // Changing the gateway will affect the VPC - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-egress-only-internet-gateway_test.go b/aws-source/adapters/ec2-egress-only-internet-gateway_test.go index 677b4b73..4f1362d0 100644 --- a/aws-source/adapters/ec2-egress-only-internet-gateway_test.go +++ b/aws-source/adapters/ec2-egress-only-internet-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestEgressOnlyInternetGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-iam-instance-profile-association.go b/aws-source/adapters/ec2-iam-instance-profile-association.go index 2f0ca0a7..7dcc6bd7 100644 --- a/aws-source/adapters/ec2-iam-instance-profile-association.go +++ b/aws-source/adapters/ec2-iam-instance-profile-association.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func iamInstanceProfileAssociationOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeIamInstanceProfileAssociationsInput, output *ec2.DescribeIamInstanceProfileAssociationsOutput) ([]*sdp.Item, error) { @@ -35,12 +35,6 @@ func iamInstanceProfileAssociationOutputMapper(_ context.Context, _ *ec2.Client, Query: *assoc.IamInstanceProfile.Arn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the profile will affect this - In: true, - // We can't affect the profile - Out: false, - }, }) } } @@ -53,12 +47,6 @@ func iamInstanceProfileAssociationOutputMapper(_ context.Context, _ *ec2.Client, Query: *assoc.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the instance will not affect the association - In: false, - // changes to the association will affect the instance - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-iam-instance-profile-association_test.go b/aws-source/adapters/ec2-iam-instance-profile-association_test.go index 72ab47a6..a0573570 100644 --- a/aws-source/adapters/ec2-iam-instance-profile-association_test.go +++ b/aws-source/adapters/ec2-iam-instance-profile-association_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestIamInstanceProfileAssociationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-image.go b/aws-source/adapters/ec2-image.go index e0ac5f24..c1052730 100644 --- a/aws-source/adapters/ec2-image.go +++ b/aws-source/adapters/ec2-image.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // ImageInputMapperGet Gets a given image. As opposed to list, get will get diff --git a/aws-source/adapters/ec2-image_test.go b/aws-source/adapters/ec2-image_test.go index b642e410..eadd9d08 100644 --- a/aws-source/adapters/ec2-image_test.go +++ b/aws-source/adapters/ec2-image_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestImageInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance-event-window.go b/aws-source/adapters/ec2-instance-event-window.go index b7db1d44..671f545d 100644 --- a/aws-source/adapters/ec2-instance-event-window.go +++ b/aws-source/adapters/ec2-instance-event-window.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func instanceEventWindowInputMapperGet(scope, query string) (*ec2.DescribeInstanceEventWindowsInput, error) { @@ -52,12 +52,6 @@ func instanceEventWindowOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the host won't affect the window - In: false, - // Changing the windows will affect the host - Out: true, - }, }) } @@ -69,12 +63,6 @@ func instanceEventWindowOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the host won't affect the window - In: false, - // Changing the windows will affect the instance - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-instance-event-window_test.go b/aws-source/adapters/ec2-instance-event-window_test.go index a8a7eb16..e0826d43 100644 --- a/aws-source/adapters/ec2-instance-event-window_test.go +++ b/aws-source/adapters/ec2-instance-event-window_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInstanceEventWindowInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance-status.go b/aws-source/adapters/ec2-instance-status.go index 0163e343..4609c94d 100644 --- a/aws-source/adapters/ec2-instance-status.go +++ b/aws-source/adapters/ec2-instance-status.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func instanceStatusInputMapperGet(scope, query string) (*ec2.DescribeInstanceStatusInput, error) { @@ -49,12 +49,6 @@ func instanceStatusOutputMapper(_ context.Context, _ *ec2.Client, scope string, Query: *instanceStatus.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The statius and the instance are closely linked and - // affect each other - In: true, - Out: true, - }, }, }, } diff --git a/aws-source/adapters/ec2-instance-status_test.go b/aws-source/adapters/ec2-instance-status_test.go index 212d16a0..f4ff907e 100644 --- a/aws-source/adapters/ec2-instance-status_test.go +++ b/aws-source/adapters/ec2-instance-status_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInstanceStatusInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance.go b/aws-source/adapters/ec2-instance.go index 3c8d8218..a6fde2ea 100644 --- a/aws-source/adapters/ec2-instance.go +++ b/aws-source/adapters/ec2-instance.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var ( @@ -62,12 +62,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The status and the instance are closely linked and - // affect each other - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -77,12 +71,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Metrics inform decisions about the instance - In: true, - // Instance changes don't affect historical metrics - Out: false, - }, }, }, } @@ -113,12 +101,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.IamInstanceProfile.Arn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the profile will affect this instance - In: true, - // We can't affect the profile - Out: false, - }, }) } } else if instance.IamInstanceProfile.Id != nil { @@ -129,12 +111,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.IamInstanceProfile.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the profile will affect this instance - In: true, - // We can't affect the profile - Out: false, - }, }) } } @@ -147,12 +123,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.CapacityReservationId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the reservation will affect the instance - In: true, - // Changing the instance won't affect the reservation - Out: false, - }, }) } @@ -165,12 +135,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *assoc.ElasticGpuId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the GPU will affect the instance - In: true, - // Changing the instance won't affect the GPU - Out: false, - }, }) } } @@ -185,12 +149,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *assoc.ElasticInferenceAcceleratorArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the accelerator will affect the instance - In: true, - // Changing the instance won't affect the accelerator - Out: false, - }, }) } } @@ -206,12 +164,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *license.LicenseConfigurationArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the license will affect the instance - In: true, - // Changing the instance won't affect the license - Out: false, - }, }) } } @@ -226,12 +178,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.OutpostArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the outpost will affect the instance - In: true, - // Changing the instance won't affect the outpost - Out: false, - }, }) } } @@ -244,12 +190,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.SpotInstanceRequestId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the spot request will affect the instance - In: true, - // Changing the instance won't affect the spot request - Out: false, - }, }) } @@ -261,12 +201,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.ImageId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the image can't affect the instance once it - // has been created - In: false, - Out: false, - }, }) } @@ -278,13 +212,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.KeyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key pair will affect your ability to - // connect to the instance - In: true, - // Changing the instance won't affect the key pair - Out: false, - }, }) } @@ -297,12 +224,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.Placement.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing a placement group will affect instances - In: true, - // Changing an instance won't affect the group - Out: false, - }, }) } } @@ -315,11 +236,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -334,11 +250,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *ip.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -352,11 +263,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *ip.PrivateIpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -370,12 +276,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *nic.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the instance - In: true, - // Changing the instance won't affect the subnet - Out: false, - }, }) } @@ -388,12 +288,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *nic.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC will affect the instance - In: true, - // Changing the instance won't affect the VPC - Out: false, - }, }) } } @@ -406,11 +300,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.PublicDnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS records are always linked - In: true, - Out: true, - }, }) } @@ -422,11 +311,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.PublicIpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always propagating - In: true, - Out: true, - }, }) } @@ -440,12 +324,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *group.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the instance - In: true, - // Changing the instance won't affect the security group - Out: false, - }, }) } } @@ -459,13 +337,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *mapping.Ebs.VolumeId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the volume will affect the instance - In: true, - // Changing the instance could also affect the - // volume since it's writing to it - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-instance_test.go b/aws-source/adapters/ec2-instance_test.go index 88288ed6..6fc7f5e9 100644 --- a/aws-source/adapters/ec2-instance_test.go +++ b/aws-source/adapters/ec2-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInstanceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-internet-gateway.go b/aws-source/adapters/ec2-internet-gateway.go index cf280e00..40f28ee1 100644 --- a/aws-source/adapters/ec2-internet-gateway.go +++ b/aws-source/adapters/ec2-internet-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func internetGatewayInputMapperGet(scope string, query string) (*ec2.DescribeInternetGatewaysInput, error) { @@ -55,12 +55,6 @@ func internetGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, Query: *attachment.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC won't affect the gateway - In: false, - // Changing the gateway will affect the VPC - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-internet-gateway_test.go b/aws-source/adapters/ec2-internet-gateway_test.go index 4d2bb563..4c6888d5 100644 --- a/aws-source/adapters/ec2-internet-gateway_test.go +++ b/aws-source/adapters/ec2-internet-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInternetGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-key-pair.go b/aws-source/adapters/ec2-key-pair.go index 75bb1795..b1ef963c 100644 --- a/aws-source/adapters/ec2-key-pair.go +++ b/aws-source/adapters/ec2-key-pair.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func keyPairInputMapperGet(scope string, query string) (*ec2.DescribeKeyPairsInput, error) { diff --git a/aws-source/adapters/ec2-key-pair_test.go b/aws-source/adapters/ec2-key-pair_test.go index 89522608..780daf80 100644 --- a/aws-source/adapters/ec2-key-pair_test.go +++ b/aws-source/adapters/ec2-key-pair_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestKeyPairInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-launch-template-version.go b/aws-source/adapters/ec2-launch-template-version.go index 4d05a4be..82f93636 100644 --- a/aws-source/adapters/ec2-launch-template-version.go +++ b/aws-source/adapters/ec2-launch-template-version.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func launchTemplateVersionInputMapperGet(scope string, query string) (*ec2.DescribeLaunchTemplateVersionsInput, error) { @@ -79,11 +79,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *ip.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -96,12 +91,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *ni.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the network interface will affect the - // template and vice versa - In: true, - Out: true, - }, }) } @@ -114,11 +103,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *ip.PrivateIpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -131,12 +115,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *ni.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the template - In: true, - // Changing the template won't affect the subnet - Out: false, - }, }) } @@ -148,14 +126,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: group, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the - // template - In: true, - // Changing the template won't affect the security - // group - Out: false, - }, }) } } @@ -168,12 +138,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *lt.ImageId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the image will affect the template - In: true, - // Changing the template won't affect the image - Out: false, - }, }) } @@ -185,12 +149,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *lt.KeyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key pair will affect the template - In: true, - // Changing the template won't affect the key pair - Out: false, - }, }) } @@ -203,12 +161,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *mapping.Ebs.SnapshotId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the snapshot will affect the template - In: true, - // Changing the template won't affect the snapshot - Out: false, - }, }) } } @@ -223,14 +175,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *target.CapacityReservationId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the capacity reservation will affect - // the template - In: true, - // Changing the template could affect the - // capacity reservation since it uses it up - Out: true, - }, }) } } @@ -245,14 +189,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *lt.Placement.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the placement group will affect the - // template - In: true, - // Changing the template won't affect the placement - // group - Out: false, - }, }) } @@ -264,12 +200,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *lt.Placement.HostId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the host will affect the template - In: true, - // Changing the template could affect the host also - Out: true, - }, }) } } @@ -282,13 +212,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the template - In: true, - // Changing the template won't affect the security - // group - Out: false, - }, }) } } diff --git a/aws-source/adapters/ec2-launch-template-version_test.go b/aws-source/adapters/ec2-launch-template-version_test.go index a86e9d43..32ac3182 100644 --- a/aws-source/adapters/ec2-launch-template-version_test.go +++ b/aws-source/adapters/ec2-launch-template-version_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLaunchTemplateVersionInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-launch-template.go b/aws-source/adapters/ec2-launch-template.go index 43afaff4..69619df5 100644 --- a/aws-source/adapters/ec2-launch-template.go +++ b/aws-source/adapters/ec2-launch-template.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func launchTemplateInputMapperGet(scope string, query string) (*ec2.DescribeLaunchTemplatesInput, error) { diff --git a/aws-source/adapters/ec2-launch-template_test.go b/aws-source/adapters/ec2-launch-template_test.go index c2f8e4ff..afa43420 100644 --- a/aws-source/adapters/ec2-launch-template_test.go +++ b/aws-source/adapters/ec2-launch-template_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLaunchTemplateInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-nat-gateway.go b/aws-source/adapters/ec2-nat-gateway.go index e6ad5c21..4c236431 100644 --- a/aws-source/adapters/ec2-nat-gateway.go +++ b/aws-source/adapters/ec2-nat-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func natGatewayInputMapperGet(scope string, query string) (*ec2.DescribeNatGatewaysInput, error) { @@ -54,12 +54,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *address.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The nat gateway and it's interfaces will affect each - // other - In: true, - Out: true, - }, }) } @@ -71,11 +65,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *address.PrivateIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } @@ -87,11 +76,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *address.PublicIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } } @@ -104,13 +88,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *ng.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet won't affect the gateway - In: false, - // Changing the gateway will affect the subnet since this - // will be gateway that subnet uses to access the internet - Out: true, - }, }) } @@ -122,12 +99,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *ng.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC could affect the gateway - In: true, - // Changing the gateway won't affect the VPC - Out: false, - }, }) } diff --git a/aws-source/adapters/ec2-nat-gateway_test.go b/aws-source/adapters/ec2-nat-gateway_test.go index 70e4f351..88e739e5 100644 --- a/aws-source/adapters/ec2-nat-gateway_test.go +++ b/aws-source/adapters/ec2-nat-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNatGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-acl.go b/aws-source/adapters/ec2-network-acl.go index 0999159f..f9952258 100644 --- a/aws-source/adapters/ec2-network-acl.go +++ b/aws-source/adapters/ec2-network-acl.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func networkAclInputMapperGet(scope string, query string) (*ec2.DescribeNetworkAclsInput, error) { @@ -54,12 +54,6 @@ func networkAclOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *assoc.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet won't affect the ACL - In: false, - // Changing the ACL will affect the subnet - Out: true, - }, }) } } @@ -72,12 +66,6 @@ func networkAclOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *networkAcl.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC won't affect the ACL - In: false, - // Changing the ACL will affect the VPC - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-network-acl_test.go b/aws-source/adapters/ec2-network-acl_test.go index a0197d24..fba190e0 100644 --- a/aws-source/adapters/ec2-network-acl_test.go +++ b/aws-source/adapters/ec2-network-acl_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNetworkAclInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-interface-permission.go b/aws-source/adapters/ec2-network-interface-permission.go index bb25592d..d7739457 100644 --- a/aws-source/adapters/ec2-network-interface-permission.go +++ b/aws-source/adapters/ec2-network-interface-permission.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func networkInterfacePermissionInputMapperGet(scope string, query string) (*ec2.DescribeNetworkInterfacePermissionsInput, error) { @@ -52,11 +52,6 @@ func networkInterfacePermissionOutputMapper(_ context.Context, _ *ec2.Client, sc Query: *ni.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These permissions are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-network-interface-permission_test.go b/aws-source/adapters/ec2-network-interface-permission_test.go index 0d07c507..93104d99 100644 --- a/aws-source/adapters/ec2-network-interface-permission_test.go +++ b/aws-source/adapters/ec2-network-interface-permission_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNetworkInterfacePermissionInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-interface.go b/aws-source/adapters/ec2-network-interface.go index bc8f5e7a..4330c164 100644 --- a/aws-source/adapters/ec2-network-interface.go +++ b/aws-source/adapters/ec2-network-interface.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func networkInterfaceInputMapperGet(scope string, query string) (*ec2.DescribeNetworkInterfacesInput, error) { @@ -96,12 +96,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ni.Attachment.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The instance and the interface are closely linked - // and affect each other - In: true, - Out: true, - }, }) } } @@ -115,12 +109,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *sg.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // A security group will affect an interface - In: true, - // An interface won't affect a security group - Out: false, - }, }) } } @@ -134,11 +122,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ip.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -153,11 +136,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *assoc.PublicDnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } @@ -169,11 +147,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *assoc.PublicIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -185,11 +158,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *assoc.CarrierIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -201,11 +169,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *assoc.CustomerOwnedIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -218,11 +181,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ip.PrivateDnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } @@ -234,11 +192,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ip.PrivateIpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -251,13 +204,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ni.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect interfaces within that - // subnet - In: true, - // Changing the interface won't affect the subnet - Out: false, - }, }) } @@ -269,12 +215,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ni.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC will affect interfaces within that VPC - In: true, - // Changing the interface won't affect the VPC - Out: false, - }, }) } diff --git a/aws-source/adapters/ec2-network-interface_test.go b/aws-source/adapters/ec2-network-interface_test.go index bbf5713b..2a03331f 100644 --- a/aws-source/adapters/ec2-network-interface_test.go +++ b/aws-source/adapters/ec2-network-interface_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNetworkInterfaceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-placement-group.go b/aws-source/adapters/ec2-placement-group.go index ec89d3cd..9685f2c2 100644 --- a/aws-source/adapters/ec2-placement-group.go +++ b/aws-source/adapters/ec2-placement-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func placementGroupInputMapperGet(scope string, query string) (*ec2.DescribePlacementGroupsInput, error) { diff --git a/aws-source/adapters/ec2-placement-group_test.go b/aws-source/adapters/ec2-placement-group_test.go index f17dfbd9..ca79deb5 100644 --- a/aws-source/adapters/ec2-placement-group_test.go +++ b/aws-source/adapters/ec2-placement-group_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestPlacementGroupInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-reserved-instance.go b/aws-source/adapters/ec2-reserved-instance.go index be7ea5d5..c6294793 100644 --- a/aws-source/adapters/ec2-reserved-instance.go +++ b/aws-source/adapters/ec2-reserved-instance.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func reservedInstanceInputMapperGet(scope, query string) (*ec2.DescribeReservedInstancesInput, error) { diff --git a/aws-source/adapters/ec2-reserved-instance_test.go b/aws-source/adapters/ec2-reserved-instance_test.go index ae67edc8..9a381de8 100644 --- a/aws-source/adapters/ec2-reserved-instance_test.go +++ b/aws-source/adapters/ec2-reserved-instance_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestReservedInstanceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-route-table.go b/aws-source/adapters/ec2-route-table.go index 733ae7bd..850d78f6 100644 --- a/aws-source/adapters/ec2-route-table.go +++ b/aws-source/adapters/ec2-route-table.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func routeTableInputMapperGet(scope string, query string) (*ec2.DescribeRouteTablesInput, error) { @@ -55,14 +55,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *assoc.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // All things in a route table could affect each other - // since changing the target could affect the - // traffic that is routed to it. And changing the route - // table could affect the target - In: true, - Out: true, - }, }) } @@ -74,10 +66,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *assoc.GatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -92,10 +80,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.GatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if strings.HasPrefix(*route.GatewayId, "vpce") { @@ -106,10 +90,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.GatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -121,10 +101,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.CarrierGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.EgressOnlyInternetGatewayId != nil { @@ -135,10 +111,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.EgressOnlyInternetGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.InstanceId != nil { @@ -149,10 +121,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.LocalGatewayId != nil { @@ -163,10 +131,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.LocalGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.NatGatewayId != nil { @@ -177,10 +141,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.NatGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.NetworkInterfaceId != nil { @@ -191,10 +151,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.TransitGatewayId != nil { @@ -205,10 +161,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.TransitGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.VpcPeeringConnectionId != nil { @@ -219,10 +171,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.VpcPeeringConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -235,10 +183,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *rt.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-route-table_test.go b/aws-source/adapters/ec2-route-table_test.go index 468e7a9a..7d5d1875 100644 --- a/aws-source/adapters/ec2-route-table_test.go +++ b/aws-source/adapters/ec2-route-table_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestRouteTableInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-security-group-rule.go b/aws-source/adapters/ec2-security-group-rule.go index fec023b6..c725c93b 100644 --- a/aws-source/adapters/ec2-security-group-rule.go +++ b/aws-source/adapters/ec2-security-group-rule.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func securityGroupRuleInputMapperGet(scope string, query string) (*ec2.DescribeSecurityGroupRulesInput, error) { @@ -53,11 +53,6 @@ func securityGroupRuleOutputMapper(_ context.Context, _ *ec2.Client, scope strin Query: *securityGroupRule.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } @@ -70,11 +65,6 @@ func securityGroupRuleOutputMapper(_ context.Context, _ *ec2.Client, scope strin Query: *rg.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-security-group-rule_test.go b/aws-source/adapters/ec2-security-group-rule_test.go index 3fdf07ad..95465917 100644 --- a/aws-source/adapters/ec2-security-group-rule_test.go +++ b/aws-source/adapters/ec2-security-group-rule_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSecurityGroupRuleInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-security-group.go b/aws-source/adapters/ec2-security-group.go index 148de33d..ee5db475 100644 --- a/aws-source/adapters/ec2-security-group.go +++ b/aws-source/adapters/ec2-security-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func securityGroupInputMapperGet(scope string, query string) (*ec2.DescribeSecurityGroupsInput, error) { @@ -55,12 +55,6 @@ func securityGroupOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ Query: *securityGroup.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the VPC could affect the security group - In: true, - // The security group won't affect the VPC though - Out: false, - }, }) } @@ -75,13 +69,6 @@ func securityGroupOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ Query: *securityGroup.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Network interfaces don't affect the security group - In: false, - // Changes to the security group affect network interfaces - // (and through them, EC2 instances) - Out: true, - }, }) } @@ -165,11 +152,6 @@ func extractLinkedSecurityGroups(permissions []types.IpPermission, scope string) Query: *idGroup.GroupId, Scope: FormatScope(relatedAccount, region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Linked security groups affect each other - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-security-group_test.go b/aws-source/adapters/ec2-security-group_test.go index 9ccb2422..ca1379a4 100644 --- a/aws-source/adapters/ec2-security-group_test.go +++ b/aws-source/adapters/ec2-security-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSecurityGroupInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-snapshot.go b/aws-source/adapters/ec2-snapshot.go index 6aae9f3f..d9c5b93f 100644 --- a/aws-source/adapters/ec2-snapshot.go +++ b/aws-source/adapters/ec2-snapshot.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func snapshotInputMapperGet(scope string, query string) (*ec2.DescribeSnapshotsInput, error) { @@ -61,14 +61,6 @@ func snapshotOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *snapshot.VolumeId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the volume will probably affect the snapshot - In: true, - // Changing the snapshot will affect the volume indirectly - // as applications might rely on snapshots as backups - // or other use-cases - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-snapshot_test.go b/aws-source/adapters/ec2-snapshot_test.go index 4bacc657..bf485224 100644 --- a/aws-source/adapters/ec2-snapshot_test.go +++ b/aws-source/adapters/ec2-snapshot_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSnapshotInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-subnet.go b/aws-source/adapters/ec2-subnet.go index a293d096..1d2a7da0 100644 --- a/aws-source/adapters/ec2-subnet.go +++ b/aws-source/adapters/ec2-subnet.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func subnetInputMapperGet(scope string, query string) (*ec2.DescribeSubnetsInput, error) { @@ -53,12 +53,6 @@ func subnetOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.D Query: *subnet.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC would affect the subnet - In: true, - // Changing the subnet won't affect the VPC - Out: false, - }, }) } diff --git a/aws-source/adapters/ec2-subnet_test.go b/aws-source/adapters/ec2-subnet_test.go index 64e121eb..693afacd 100644 --- a/aws-source/adapters/ec2-subnet_test.go +++ b/aws-source/adapters/ec2-subnet_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSubnetInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-association.go b/aws-source/adapters/ec2-transit-gateway-route-table-association.go new file mode 100644 index 00000000..421becf5 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table-association.go @@ -0,0 +1,255 @@ +package adapters + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// APIs used: +// - DescribeTransitGatewayRouteTables — list route tables (to then fetch associations per table). +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html +// - GetTransitGatewayRouteTableAssociations — list associations for a route table. +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetTransitGatewayRouteTableAssociations.html + +// transitGatewayRouteTableAssociationItem holds an association plus its route table ID for unique identification. +type transitGatewayRouteTableAssociationItem struct { + RouteTableID string + Association types.TransitGatewayRouteTableAssociation +} + +const associationIDSep = "|" + +func transitGatewayRouteTableAssociationID(routeTableID, attachmentID string) string { + return routeTableID + associationIDSep + attachmentID +} + +// parseCompositeID splits query by the given separator; accepts both `|` and `_` (Terraform uses `_`). +// Returns (left, right); empty left means invalid. +func parseCompositeID(query, sep string) (string, string) { + parts := strings.SplitN(query, sep, 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "" + } + return parts[0], parts[1] +} + +func parseAssociationQuery(query string) (routeTableID, attachmentID string, err error) { + if a, b := parseCompositeID(query, associationIDSep); a != "" { + return a, b, nil + } + if a, b := parseCompositeID(query, "_"); a != "" { + return a, b, nil + } + return "", "", fmt.Errorf("query must be TransitGatewayRouteTableId|TransitGatewayAttachmentId") +} + +func getTransitGatewayRouteTableAssociation(ctx context.Context, client *ec2.Client, _, query string) (*transitGatewayRouteTableAssociationItem, error) { + routeTableID, attachmentID, err := parseAssociationQuery(query) + if err != nil { + return nil, err + } + pg := ec2.NewGetTransitGatewayRouteTableAssociationsPaginator(client, &ec2.GetTransitGatewayRouteTableAssociationsInput{ + TransitGatewayRouteTableId: &routeTableID, + }) + for pg.HasMorePages() { + out, err := pg.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range out.Associations { + a := &out.Associations[i] + if a.TransitGatewayAttachmentId != nil && *a.TransitGatewayAttachmentId == attachmentID { + return &transitGatewayRouteTableAssociationItem{RouteTableID: routeTableID, Association: *a}, nil + } + } + } + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("association %s not found", query), + } +} + +func listTransitGatewayRouteTableAssociations(ctx context.Context, client *ec2.Client, _ string) ([]*transitGatewayRouteTableAssociationItem, error) { + // List all route tables, then get associations for each. + rtPaginator := ec2.NewDescribeTransitGatewayRouteTablesPaginator(client, &ec2.DescribeTransitGatewayRouteTablesInput{}) + var items []*transitGatewayRouteTableAssociationItem + for rtPaginator.HasMorePages() { + rtOut, err := rtPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for _, rt := range rtOut.TransitGatewayRouteTables { + if rt.TransitGatewayRouteTableId == nil { + continue + } + rtID := *rt.TransitGatewayRouteTableId + assocPaginator := ec2.NewGetTransitGatewayRouteTableAssociationsPaginator(client, &ec2.GetTransitGatewayRouteTableAssociationsInput{ + TransitGatewayRouteTableId: &rtID, + }) + for assocPaginator.HasMorePages() { + assocOut, err := assocPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range assocOut.Associations { + items = append(items, &transitGatewayRouteTableAssociationItem{ + RouteTableID: rtID, + Association: assocOut.Associations[i], + }) + } + } + } + } + return items, nil +} + +// searchTransitGatewayRouteTableAssociations returns all associations for a single route table. +// Query must be a TransitGatewayRouteTableId (e.g. tgw-rtb-xxxxx). +func searchTransitGatewayRouteTableAssociations(ctx context.Context, client *ec2.Client, _, query string) ([]*transitGatewayRouteTableAssociationItem, error) { + routeTableID := query + var items []*transitGatewayRouteTableAssociationItem + pg := ec2.NewGetTransitGatewayRouteTableAssociationsPaginator(client, &ec2.GetTransitGatewayRouteTableAssociationsInput{ + TransitGatewayRouteTableId: &routeTableID, + }) + for pg.HasMorePages() { + out, err := pg.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range out.Associations { + items = append(items, &transitGatewayRouteTableAssociationItem{ + RouteTableID: routeTableID, + Association: out.Associations[i], + }) + } + } + return items, nil +} + +func transitGatewayRouteTableAssociationItemMapper(query, scope string, awsItem *transitGatewayRouteTableAssociationItem) (*sdp.Item, error) { + a := &awsItem.Association + attrs, err := ToAttributesWithExclude(a, "") + if err != nil { + return nil, err + } + attachmentID := "" + if a.TransitGatewayAttachmentId != nil { + attachmentID = *a.TransitGatewayAttachmentId + } + uniqueVal := transitGatewayRouteTableAssociationID(awsItem.RouteTableID, attachmentID) + if err := attrs.Set("TransitGatewayRouteTableIdWithTransitGatewayAttachmentId", uniqueVal); err != nil { + return nil, err + } + item := &sdp.Item{ + Type: "ec2-transit-gateway-route-table-association", + UniqueAttribute: "TransitGatewayRouteTableIdWithTransitGatewayAttachmentId", + Scope: scope, + Attributes: attrs, + } + // Link to route table + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table", + Method: sdp.QueryMethod_GET, + Query: awsItem.RouteTableID, + Scope: scope, + }, + }) + if a.TransitGatewayAttachmentId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-attachment", + Method: sdp.QueryMethod_GET, + Query: *a.TransitGatewayAttachmentId, + Scope: scope, + }, + }) + } + if a.ResourceId != nil && *a.ResourceId != "" { + switch a.ResourceType { + case types.TransitGatewayAttachmentResourceTypeVpc: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpc", + Method: sdp.QueryMethod_GET, + Query: *a.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeVpn: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpn-connection", + Method: sdp.QueryMethod_GET, + Query: *a.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "directconnect-direct-connect-gateway", + Method: sdp.QueryMethod_GET, + Query: *a.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypePeering, + types.TransitGatewayAttachmentResourceTypeTgwPeering: + // ResourceId is the peer transit gateway ID (e.g. tgw-xxxxx). + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway", + Method: sdp.QueryMethod_GET, + Query: *a.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, + types.TransitGatewayAttachmentResourceTypeConnect, + types.TransitGatewayAttachmentResourceTypeNetworkFunction: + // No Overmind adapter for these resource types; attachment link above is sufficient. + } + } + return item, nil +} + +func NewEC2TransitGatewayRouteTableAssociationAdapter(client *ec2.Client, accountID, region string, cache sdpcache.Cache) *GetListAdapter[*transitGatewayRouteTableAssociationItem, *ec2.Client, *ec2.Options] { + return &GetListAdapter[*transitGatewayRouteTableAssociationItem, *ec2.Client, *ec2.Options]{ + ItemType: "ec2-transit-gateway-route-table-association", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: transitGatewayRouteTableAssociationAdapterMetadata, + cache: cache, + GetFunc: getTransitGatewayRouteTableAssociation, + ListFunc: listTransitGatewayRouteTableAssociations, + SearchFunc: searchTransitGatewayRouteTableAssociations, + ItemMapper: transitGatewayRouteTableAssociationItemMapper, + } +} + +var transitGatewayRouteTableAssociationAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "ec2-transit-gateway-route-table-association", + DescriptiveName: "Transit Gateway Route Table Association", + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get by TransitGatewayRouteTableId|TransitGatewayAttachmentId", + ListDescription: "List all route table associations", + SearchDescription: "Search by TransitGatewayRouteTableId to list associations for that route table", + }, + PotentialLinks: []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table", "ec2-transit-gateway-attachment", "ec2-vpc", "ec2-vpn-connection", "directconnect-direct-connect-gateway"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_ec2_transit_gateway_route_table_association.id"}, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, +}) diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-association_test.go b/aws-source/adapters/ec2-transit-gateway-route-table-association_test.go new file mode 100644 index 00000000..b6447a84 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table-association_test.go @@ -0,0 +1,64 @@ +package adapters + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestParseAssociationQuery(t *testing.T) { + rt, att, err := parseAssociationQuery("tgw-rtb-1|tgw-attach-2") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || att != "tgw-attach-2" { + t.Errorf("expected tgw-rtb-1, tgw-attach-2 got %q, %q", rt, att) + } + // Terraform uses underscore as separator + rt, att, err = parseAssociationQuery("tgw-rtb-1_tgw-attach-2") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || att != "tgw-attach-2" { + t.Errorf("expected tgw-rtb-1, tgw-attach-2 (underscore) got %q, %q", rt, att) + } + _, _, err = parseAssociationQuery("bad") + if err == nil { + t.Error("expected error for bad query") + } +} + +func TestTransitGatewayRouteTableAssociationItemMapper(t *testing.T) { + item := &transitGatewayRouteTableAssociationItem{ + RouteTableID: "tgw-rtb-123", + Association: types.TransitGatewayRouteTableAssociation{ + TransitGatewayAttachmentId: PtrString("tgw-attach-456"), + ResourceId: PtrString("vpc-abc"), + ResourceType: types.TransitGatewayAttachmentResourceTypeVpc, + State: types.TransitGatewayAssociationStateAssociated, + }, + } + sdpItem, err := transitGatewayRouteTableAssociationItemMapper("", "account|region", item) + if err != nil { + t.Fatal(err) + } + if err := sdpItem.Validate(); err != nil { + t.Error(err) + } + if sdpItem.GetType() != "ec2-transit-gateway-route-table-association" { + t.Errorf("unexpected type %s", sdpItem.GetType()) + } + uv, _ := sdpItem.GetAttributes().Get("TransitGatewayRouteTableIdWithTransitGatewayAttachmentId") + if uv != "tgw-rtb-123|tgw-attach-456" { + t.Errorf("unexpected unique value %v", uv) + } +} + +func TestNewEC2TransitGatewayRouteTableAssociationAdapter(t *testing.T) { + client, account, region := ec2GetAutoConfig(t) + adapter := NewEC2TransitGatewayRouteTableAssociationAdapter(client, account, region, sdpcache.NewNoOpCache()) + if err := adapter.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go b/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go new file mode 100644 index 00000000..4a321847 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go @@ -0,0 +1,252 @@ +package adapters + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// APIs used: +// - DescribeTransitGatewayRouteTables — list route tables (to then fetch propagations per table). +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html +// - GetTransitGatewayRouteTablePropagations — list propagations for a route table. +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetTransitGatewayRouteTablePropagations.html + +type transitGatewayRouteTablePropagationItem struct { + RouteTableID string + Propagation types.TransitGatewayRouteTablePropagation +} + +const propagationIDSep = "|" + +func transitGatewayRouteTablePropagationID(routeTableID, attachmentID string) string { + return routeTableID + propagationIDSep + attachmentID +} + +func parsePropagationQuery(query string) (routeTableID, attachmentID string, err error) { + if a, b := parseCompositeID(query, propagationIDSep); a != "" { + return a, b, nil + } + if a, b := parseCompositeID(query, "_"); a != "" { + return a, b, nil + } + return "", "", fmt.Errorf("query must be TransitGatewayRouteTableId|TransitGatewayAttachmentId") +} + +func getTransitGatewayRouteTablePropagation(ctx context.Context, client *ec2.Client, _, query string) (*transitGatewayRouteTablePropagationItem, error) { + routeTableID, attachmentID, err := parsePropagationQuery(query) + if err != nil { + return nil, err + } + pg := ec2.NewGetTransitGatewayRouteTablePropagationsPaginator(client, &ec2.GetTransitGatewayRouteTablePropagationsInput{ + TransitGatewayRouteTableId: &routeTableID, + }) + for pg.HasMorePages() { + out, err := pg.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range out.TransitGatewayRouteTablePropagations { + p := &out.TransitGatewayRouteTablePropagations[i] + if p.TransitGatewayAttachmentId != nil && *p.TransitGatewayAttachmentId == attachmentID { + return &transitGatewayRouteTablePropagationItem{RouteTableID: routeTableID, Propagation: *p}, nil + } + } + } + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("propagation %s not found", query), + } +} + +func listTransitGatewayRouteTablePropagations(ctx context.Context, client *ec2.Client, _ string) ([]*transitGatewayRouteTablePropagationItem, error) { + rtPaginator := ec2.NewDescribeTransitGatewayRouteTablesPaginator(client, &ec2.DescribeTransitGatewayRouteTablesInput{}) + var items []*transitGatewayRouteTablePropagationItem + for rtPaginator.HasMorePages() { + rtOut, err := rtPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for _, rt := range rtOut.TransitGatewayRouteTables { + if rt.TransitGatewayRouteTableId == nil { + continue + } + rtID := *rt.TransitGatewayRouteTableId + propPaginator := ec2.NewGetTransitGatewayRouteTablePropagationsPaginator(client, &ec2.GetTransitGatewayRouteTablePropagationsInput{ + TransitGatewayRouteTableId: &rtID, + }) + for propPaginator.HasMorePages() { + propOut, err := propPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range propOut.TransitGatewayRouteTablePropagations { + items = append(items, &transitGatewayRouteTablePropagationItem{ + RouteTableID: rtID, + Propagation: propOut.TransitGatewayRouteTablePropagations[i], + }) + } + } + } + } + return items, nil +} + +// searchTransitGatewayRouteTablePropagations returns all propagations for a single route table. +// Query must be a TransitGatewayRouteTableId (e.g. tgw-rtb-xxxxx). +func searchTransitGatewayRouteTablePropagations(ctx context.Context, client *ec2.Client, _, query string) ([]*transitGatewayRouteTablePropagationItem, error) { + routeTableID := query + var items []*transitGatewayRouteTablePropagationItem + pg := ec2.NewGetTransitGatewayRouteTablePropagationsPaginator(client, &ec2.GetTransitGatewayRouteTablePropagationsInput{ + TransitGatewayRouteTableId: &routeTableID, + }) + for pg.HasMorePages() { + out, err := pg.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range out.TransitGatewayRouteTablePropagations { + items = append(items, &transitGatewayRouteTablePropagationItem{ + RouteTableID: routeTableID, + Propagation: out.TransitGatewayRouteTablePropagations[i], + }) + } + } + return items, nil +} + +func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem *transitGatewayRouteTablePropagationItem) (*sdp.Item, error) { + p := &awsItem.Propagation + attrs, err := ToAttributesWithExclude(p, "") + if err != nil { + return nil, err + } + attachmentID := "" + if p.TransitGatewayAttachmentId != nil { + attachmentID = *p.TransitGatewayAttachmentId + } + uniqueVal := transitGatewayRouteTablePropagationID(awsItem.RouteTableID, attachmentID) + if err := attrs.Set("TransitGatewayRouteTableIdWithTransitGatewayAttachmentId", uniqueVal); err != nil { + return nil, err + } + item := &sdp.Item{ + Type: "ec2-transit-gateway-route-table-propagation", + UniqueAttribute: "TransitGatewayRouteTableIdWithTransitGatewayAttachmentId", + Scope: scope, + Attributes: attrs, + } + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table", + Method: sdp.QueryMethod_GET, + Query: awsItem.RouteTableID, + Scope: scope, + }, + }) + // Link to the route table association (same route table + attachment). + if p.TransitGatewayAttachmentId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table-association", + Method: sdp.QueryMethod_GET, + Query: transitGatewayRouteTableAssociationID(awsItem.RouteTableID, *p.TransitGatewayAttachmentId), + Scope: scope, + }, + }) + } + if p.TransitGatewayAttachmentId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-attachment", + Method: sdp.QueryMethod_GET, + Query: *p.TransitGatewayAttachmentId, + Scope: scope, + }, + }) + } + if p.ResourceId != nil && *p.ResourceId != "" { + switch p.ResourceType { + case types.TransitGatewayAttachmentResourceTypeVpc: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpc", + Method: sdp.QueryMethod_GET, + Query: *p.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeVpn: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpn-connection", + Method: sdp.QueryMethod_GET, + Query: *p.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "directconnect-direct-connect-gateway", + Method: sdp.QueryMethod_GET, + Query: *p.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypePeering, + types.TransitGatewayAttachmentResourceTypeTgwPeering: + // ResourceId is the peer transit gateway ID (e.g. tgw-xxxxx). + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway", + Method: sdp.QueryMethod_GET, + Query: *p.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, + types.TransitGatewayAttachmentResourceTypeConnect, + types.TransitGatewayAttachmentResourceTypeNetworkFunction: + // No Overmind adapter for these resource types; attachment link above is sufficient. + } + } + return item, nil +} + +func NewEC2TransitGatewayRouteTablePropagationAdapter(client *ec2.Client, accountID, region string, cache sdpcache.Cache) *GetListAdapter[*transitGatewayRouteTablePropagationItem, *ec2.Client, *ec2.Options] { + return &GetListAdapter[*transitGatewayRouteTablePropagationItem, *ec2.Client, *ec2.Options]{ + ItemType: "ec2-transit-gateway-route-table-propagation", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: transitGatewayRouteTablePropagationAdapterMetadata, + cache: cache, + GetFunc: getTransitGatewayRouteTablePropagation, + ListFunc: listTransitGatewayRouteTablePropagations, + SearchFunc: searchTransitGatewayRouteTablePropagations, + ItemMapper: transitGatewayRouteTablePropagationItemMapper, + } +} + +var transitGatewayRouteTablePropagationAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "ec2-transit-gateway-route-table-propagation", + DescriptiveName: "Transit Gateway Route Table Propagation", + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get by TransitGatewayRouteTableId|TransitGatewayAttachmentId", + ListDescription: "List all route table propagations", + SearchDescription: "Search by TransitGatewayRouteTableId to list propagations for that route table", + }, + PotentialLinks: []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table", "ec2-transit-gateway-route-table-association", "ec2-transit-gateway-attachment", "ec2-vpc", "ec2-vpn-connection", "directconnect-direct-connect-gateway"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_ec2_transit_gateway_route_table_propagation.id"}, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, +}) diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go b/aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go new file mode 100644 index 00000000..1d2e6b9f --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go @@ -0,0 +1,60 @@ +package adapters + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestParsePropagationQuery(t *testing.T) { + rt, att, err := parsePropagationQuery("tgw-rtb-1|tgw-attach-2") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || att != "tgw-attach-2" { + t.Errorf("expected tgw-rtb-1, tgw-attach-2 got %q, %q", rt, att) + } + // Terraform uses underscore as separator + rt, att, err = parsePropagationQuery("tgw-rtb-1_tgw-attach-2") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || att != "tgw-attach-2" { + t.Errorf("expected tgw-rtb-1, tgw-attach-2 (underscore) got %q, %q", rt, att) + } + _, _, err = parsePropagationQuery("bad") + if err == nil { + t.Error("expected error for bad query") + } +} + +func TestTransitGatewayRouteTablePropagationItemMapper(t *testing.T) { + item := &transitGatewayRouteTablePropagationItem{ + RouteTableID: "tgw-rtb-123", + Propagation: types.TransitGatewayRouteTablePropagation{ + TransitGatewayAttachmentId: PtrString("tgw-attach-456"), + ResourceId: PtrString("vpc-abc"), + ResourceType: types.TransitGatewayAttachmentResourceTypeVpc, + State: types.TransitGatewayPropagationStateEnabled, + }, + } + sdpItem, err := transitGatewayRouteTablePropagationItemMapper("", "account|region", item) + if err != nil { + t.Fatal(err) + } + if err := sdpItem.Validate(); err != nil { + t.Error(err) + } + if sdpItem.GetType() != "ec2-transit-gateway-route-table-propagation" { + t.Errorf("unexpected type %s", sdpItem.GetType()) + } +} + +func TestNewEC2TransitGatewayRouteTablePropagationAdapter(t *testing.T) { + client, account, region := ec2GetAutoConfig(t) + adapter := NewEC2TransitGatewayRouteTablePropagationAdapter(client, account, region, sdpcache.NewNoOpCache()) + if err := adapter.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/aws-source/adapters/ec2-transit-gateway-route-table.go b/aws-source/adapters/ec2-transit-gateway-route-table.go new file mode 100644 index 00000000..8cdeb0c8 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table.go @@ -0,0 +1,120 @@ +package adapters + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// APIs used: +// - DescribeTransitGatewayRouteTables — list/describe transit gateway route tables. +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html + +func transitGatewayRouteTableInputMapperGet(scope string, query string) (*ec2.DescribeTransitGatewayRouteTablesInput, error) { + return &ec2.DescribeTransitGatewayRouteTablesInput{ + TransitGatewayRouteTableIds: []string{ + query, + }, + }, nil +} + +func transitGatewayRouteTableInputMapperList(scope string) (*ec2.DescribeTransitGatewayRouteTablesInput, error) { + return &ec2.DescribeTransitGatewayRouteTablesInput{}, nil +} + +func transitGatewayRouteTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeTransitGatewayRouteTablesInput, output *ec2.DescribeTransitGatewayRouteTablesOutput) ([]*sdp.Item, error) { + items := make([]*sdp.Item, 0) + + for _, rt := range output.TransitGatewayRouteTables { + var err error + var attrs *sdp.ItemAttributes + attrs, err = ToAttributesWithExclude(rt, "tags") + + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + Scope: scope, + } + } + + item := sdp.Item{ + Type: "ec2-transit-gateway-route-table", + UniqueAttribute: "TransitGatewayRouteTableId", + Scope: scope, + Attributes: attrs, + Tags: ec2TagsToMap(rt.Tags), + } + + if rt.TransitGatewayId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway", + Method: sdp.QueryMethod_GET, + Query: *rt.TransitGatewayId, + Scope: scope, + }, + }) + } + + // Link to route table associations, propagations, and routes (Search by route table ID). + if rt.TransitGatewayRouteTableId != nil { + rtID := *rt.TransitGatewayRouteTableId + for _, linkType := range []string{"ec2-transit-gateway-route-table-association", "ec2-transit-gateway-route-table-propagation", "ec2-transit-gateway-route"} { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: linkType, + Method: sdp.QueryMethod_SEARCH, + Query: rtID, + Scope: scope, + }, + }) + } + } + + items = append(items, &item) + } + + return items, nil +} + +func NewEC2TransitGatewayRouteTableAdapter(client *ec2.Client, accountID string, region string, cache sdpcache.Cache) *DescribeOnlyAdapter[*ec2.DescribeTransitGatewayRouteTablesInput, *ec2.DescribeTransitGatewayRouteTablesOutput, *ec2.Client, *ec2.Options] { + return &DescribeOnlyAdapter[*ec2.DescribeTransitGatewayRouteTablesInput, *ec2.DescribeTransitGatewayRouteTablesOutput, *ec2.Client, *ec2.Options]{ + Region: region, + Client: client, + AccountID: accountID, + ItemType: "ec2-transit-gateway-route-table", + AdapterMetadata: transitGatewayRouteTableAdapterMetadata, + cache: cache, + DescribeFunc: func(ctx context.Context, client *ec2.Client, input *ec2.DescribeTransitGatewayRouteTablesInput) (*ec2.DescribeTransitGatewayRouteTablesOutput, error) { + return client.DescribeTransitGatewayRouteTables(ctx, input) + }, + InputMapperGet: transitGatewayRouteTableInputMapperGet, + InputMapperList: transitGatewayRouteTableInputMapperList, + PaginatorBuilder: func(client *ec2.Client, params *ec2.DescribeTransitGatewayRouteTablesInput) Paginator[*ec2.DescribeTransitGatewayRouteTablesOutput, *ec2.Options] { + return ec2.NewDescribeTransitGatewayRouteTablesPaginator(client, params) + }, + OutputMapper: transitGatewayRouteTableOutputMapper, + } +} + +var transitGatewayRouteTableAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "ec2-transit-gateway-route-table", + DescriptiveName: "Transit Gateway Route Table", + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get a transit gateway route table by ID", + ListDescription: "List all transit gateway route tables", + SearchDescription: "Search transit gateway route tables by ARN", + }, + PotentialLinks: []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table-association", "ec2-transit-gateway-route-table-propagation", "ec2-transit-gateway-route"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_ec2_transit_gateway_route_table.id"}, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, +}) diff --git a/aws-source/adapters/ec2-transit-gateway-route-table_test.go b/aws-source/adapters/ec2-transit-gateway-route-table_test.go new file mode 100644 index 00000000..2bf596fe --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table_test.go @@ -0,0 +1,111 @@ +package adapters + +import ( + "context" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestTransitGatewayRouteTableInputMapperGet(t *testing.T) { + input, err := transitGatewayRouteTableInputMapperGet("foo", "tgw-rtb-123") + + if err != nil { + t.Error(err) + } + + if len(input.TransitGatewayRouteTableIds) != 1 { + t.Fatalf("expected 1 TransitGatewayRouteTable ID, got %v", len(input.TransitGatewayRouteTableIds)) + } + + if input.TransitGatewayRouteTableIds[0] != "tgw-rtb-123" { + t.Errorf("expected TransitGatewayRouteTable ID to be tgw-rtb-123, got %v", input.TransitGatewayRouteTableIds[0]) + } +} + +func TestTransitGatewayRouteTableInputMapperList(t *testing.T) { + input, err := transitGatewayRouteTableInputMapperList("foo") + + if err != nil { + t.Error(err) + } + + if len(input.Filters) != 0 || len(input.TransitGatewayRouteTableIds) != 0 { + t.Errorf("non-empty input: %v", input) + } +} + +func TestTransitGatewayRouteTableOutputMapper(t *testing.T) { + output := &ec2.DescribeTransitGatewayRouteTablesOutput{ + TransitGatewayRouteTables: []types.TransitGatewayRouteTable{ + { + TransitGatewayRouteTableId: PtrString("tgw-rtb-0123456789abcdef0"), + TransitGatewayId: PtrString("tgw-0abc123"), + State: types.TransitGatewayRouteTableStateAvailable, + DefaultAssociationRouteTable: PtrBool(false), + DefaultPropagationRouteTable: PtrBool(false), + Tags: []types.Tag{ + {Key: PtrString("Name"), Value: PtrString("my-route-table")}, + }, + }, + }, + } + + items, err := transitGatewayRouteTableOutputMapper(context.Background(), nil, "foo", nil, output) + + if err != nil { + t.Fatal(err) + } + + for _, item := range items { + if err := item.Validate(); err != nil { + t.Error(err) + } + } + + if len(items) != 1 { + t.Fatalf("expected 1 item, got %v", len(items)) + } + + if items[0].GetUniqueAttribute() != "TransitGatewayRouteTableId" { + t.Errorf("expected UniqueAttribute TransitGatewayRouteTableId, got %v", items[0].GetUniqueAttribute()) + } + + // Should link to ec2-transit-gateway and to associations, propagations, routes (Search by route table ID) + links := items[0].GetLinkedItemQueries() + if len(links) != 4 { + t.Fatalf("expected 4 linked item queries (ec2-transit-gateway + 3 Search), got %v", len(links)) + } + if links[0].GetQuery().GetType() != "ec2-transit-gateway" { + t.Errorf("expected first link type ec2-transit-gateway, got %v", links[0].GetQuery().GetType()) + } + searchTypes := map[string]bool{} + for _, l := range links[1:] { + if l.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("expected Search method for link %s", l.GetQuery().GetType()) + } + searchTypes[l.GetQuery().GetType()] = true + } + for _, want := range []string{"ec2-transit-gateway-route-table-association", "ec2-transit-gateway-route-table-propagation", "ec2-transit-gateway-route"} { + if !searchTypes[want] { + t.Errorf("expected Search link to %s", want) + } + } +} + +func TestNewEC2TransitGatewayRouteTableAdapter(t *testing.T) { + client, account, region := ec2GetAutoConfig(t) + + adapter := NewEC2TransitGatewayRouteTableAdapter(client, account, region, sdpcache.NewNoOpCache()) + + test := E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + } + + test.Run(t) +} diff --git a/aws-source/adapters/ec2-transit-gateway-route.go b/aws-source/adapters/ec2-transit-gateway-route.go new file mode 100644 index 00000000..c7f10314 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route.go @@ -0,0 +1,300 @@ +package adapters + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// APIs used: +// - DescribeTransitGatewayRouteTables — list route tables (to then search routes per table). +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html +// - SearchTransitGatewayRoutes — search routes in a route table. +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SearchTransitGatewayRoutes.html +// +// Note: SearchTransitGatewayRoutes does not support NextToken-based pagination. It returns +// at most 1000 routes per call; AdditionalRoutesAvailable indicates more exist but there is +// no API mechanism to fetch them (route tables can hold up to 10,000 routes). + +type transitGatewayRouteItem struct { + RouteTableID string + Route types.TransitGatewayRoute +} + +const routeIDSep = "|" +const routeDestPrefixList = "pl:" + +func transitGatewayRouteDestination(r *types.TransitGatewayRoute) string { + if r.PrefixListId != nil && *r.PrefixListId != "" { + return routeDestPrefixList + *r.PrefixListId + } + if r.DestinationCidrBlock != nil { + return *r.DestinationCidrBlock + } + return "" +} + +func transitGatewayRouteID(routeTableID, destination string) string { + return routeTableID + routeIDSep + destination +} + +func parseRouteQuery(query string) (routeTableID, destination string, err error) { + if a, b := parseCompositeID(query, routeIDSep); a != "" { + return a, b, nil + } + if a, b := parseCompositeID(query, "_"); a != "" { + return a, b, nil + } + return "", "", fmt.Errorf("query must be TransitGatewayRouteTableId|Destination (CIDR or pl:PrefixListId)") +} + +// searchRoutesFilter returns a filter that returns all routes (active and blackhole). +func searchRoutesFilter() []types.Filter { + return []types.Filter{ + {Name: PtrString("state"), Values: []string{"active", "blackhole"}}, + } +} + +// maxSearchRoutesResults is the maximum routes SearchTransitGatewayRoutes returns per call. +// The API does not support NextToken pagination when AdditionalRoutesAvailable is true. +const maxSearchRoutesResults = 1000 + +func getTransitGatewayRoute(ctx context.Context, client *ec2.Client, _, query string) (*transitGatewayRouteItem, error) { + routeTableID, destination, err := parseRouteQuery(query) + if err != nil { + return nil, err + } + out, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ + TransitGatewayRouteTableId: &routeTableID, + Filters: searchRoutesFilter(), + MaxResults: PtrInt32(maxSearchRoutesResults), + }) + if err != nil { + return nil, err + } + for i := range out.Routes { + r := &out.Routes[i] + if transitGatewayRouteDestination(r) == destination { + return &transitGatewayRouteItem{RouteTableID: routeTableID, Route: *r}, nil + } + } + errStr := fmt.Sprintf("route %s not found", query) + if out.AdditionalRoutesAvailable != nil && *out.AdditionalRoutesAvailable { + errStr = fmt.Sprintf("route %s not found in first %d routes; route table has additional routes that cannot be retrieved via this API", query, maxSearchRoutesResults) + } + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: errStr, + } +} + +func listTransitGatewayRoutes(ctx context.Context, client *ec2.Client, _ string) ([]*transitGatewayRouteItem, error) { + rtPaginator := ec2.NewDescribeTransitGatewayRouteTablesPaginator(client, &ec2.DescribeTransitGatewayRouteTablesInput{}) + var items []*transitGatewayRouteItem + for rtPaginator.HasMorePages() { + rtOut, err := rtPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for _, rt := range rtOut.TransitGatewayRouteTables { + if rt.TransitGatewayRouteTableId == nil { + continue + } + rtID := *rt.TransitGatewayRouteTableId + // Single call per route table: SearchTransitGatewayRoutes returns at most 1000 routes + // and does not support NextToken pagination; AdditionalRoutesAvailable means more + // exist but cannot be fetched via this API. + routeOut, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ + TransitGatewayRouteTableId: &rtID, + Filters: searchRoutesFilter(), + MaxResults: PtrInt32(maxSearchRoutesResults), + }) + if err != nil { + return nil, err + } + for i := range routeOut.Routes { + items = append(items, &transitGatewayRouteItem{ + RouteTableID: rtID, + Route: routeOut.Routes[i], + }) + } + } + } + return items, nil +} + +// searchTransitGatewayRoutes returns all routes for a single route table. +// Query must be a TransitGatewayRouteTableId (e.g. tgw-rtb-xxxxx). +func searchTransitGatewayRoutes(ctx context.Context, client *ec2.Client, _, query string) ([]*transitGatewayRouteItem, error) { + routeTableID := query + routeOut, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ + TransitGatewayRouteTableId: &routeTableID, + Filters: searchRoutesFilter(), + MaxResults: PtrInt32(maxSearchRoutesResults), + }) + if err != nil { + return nil, err + } + items := make([]*transitGatewayRouteItem, 0, len(routeOut.Routes)) + for i := range routeOut.Routes { + items = append(items, &transitGatewayRouteItem{ + RouteTableID: routeTableID, + Route: routeOut.Routes[i], + }) + } + return items, nil +} + +func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayRouteItem) (*sdp.Item, error) { + r := &awsItem.Route + attrs, err := ToAttributesWithExclude(r, "") + if err != nil { + return nil, err + } + dest := transitGatewayRouteDestination(r) + uniqueVal := transitGatewayRouteID(awsItem.RouteTableID, dest) + if err := attrs.Set("TransitGatewayRouteTableIdWithDestination", uniqueVal); err != nil { + return nil, err + } + item := &sdp.Item{ + Type: "ec2-transit-gateway-route", + UniqueAttribute: "TransitGatewayRouteTableIdWithDestination", + Scope: scope, + Attributes: attrs, + } + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table", + Method: sdp.QueryMethod_GET, + Query: awsItem.RouteTableID, + Scope: scope, + }, + }) + for i := range r.TransitGatewayAttachments { + att := &r.TransitGatewayAttachments[i] + if att.TransitGatewayAttachmentId != nil && *att.TransitGatewayAttachmentId != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-attachment", + Method: sdp.QueryMethod_GET, + Query: *att.TransitGatewayAttachmentId, + Scope: scope, + }, + }) + // Link to the route table association (same route table + attachment). + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table-association", + Method: sdp.QueryMethod_GET, + Query: transitGatewayRouteTableAssociationID(awsItem.RouteTableID, *att.TransitGatewayAttachmentId), + Scope: scope, + }, + }) + } + if att.ResourceId != nil && *att.ResourceId != "" { + switch att.ResourceType { + case types.TransitGatewayAttachmentResourceTypeVpc: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpc", + Method: sdp.QueryMethod_GET, + Query: *att.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeVpn: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpn-connection", + Method: sdp.QueryMethod_GET, + Query: *att.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "directconnect-direct-connect-gateway", + Method: sdp.QueryMethod_GET, + Query: *att.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypePeering, + types.TransitGatewayAttachmentResourceTypeTgwPeering: + // ResourceId is the peer transit gateway ID (e.g. tgw-xxxxx). + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway", + Method: sdp.QueryMethod_GET, + Query: *att.ResourceId, + Scope: scope, + }, + }) + case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, + types.TransitGatewayAttachmentResourceTypeConnect, + types.TransitGatewayAttachmentResourceTypeNetworkFunction: + // No Overmind adapter for these; attachment link above is sufficient. + } + } + } + if r.PrefixListId != nil && *r.PrefixListId != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-managed-prefix-list", + Method: sdp.QueryMethod_GET, + Query: *r.PrefixListId, + Scope: scope, + }, + }) + } + if r.TransitGatewayRouteTableAnnouncementId != nil && *r.TransitGatewayRouteTableAnnouncementId != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table-announcement", + Method: sdp.QueryMethod_GET, + Query: *r.TransitGatewayRouteTableAnnouncementId, + Scope: scope, + }, + }) + } + return item, nil +} + +func NewEC2TransitGatewayRouteAdapter(client *ec2.Client, accountID, region string, cache sdpcache.Cache) *GetListAdapter[*transitGatewayRouteItem, *ec2.Client, *ec2.Options] { + return &GetListAdapter[*transitGatewayRouteItem, *ec2.Client, *ec2.Options]{ + ItemType: "ec2-transit-gateway-route", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: transitGatewayRouteAdapterMetadata, + cache: cache, + GetFunc: getTransitGatewayRoute, + ListFunc: listTransitGatewayRoutes, + SearchFunc: searchTransitGatewayRoutes, + ItemMapper: transitGatewayRouteItemMapper, + } +} + +var transitGatewayRouteAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "ec2-transit-gateway-route", + DescriptiveName: "Transit Gateway Route", + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get by TransitGatewayRouteTableId|Destination (CIDR or pl:PrefixListId)", + ListDescription: "List all transit gateway routes", + SearchDescription: "Search by TransitGatewayRouteTableId to list routes for that route table", + }, + PotentialLinks: []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table", "ec2-transit-gateway-route-table-association", "ec2-transit-gateway-attachment", "ec2-transit-gateway-route-table-announcement", "ec2-vpc", "ec2-vpn-connection", "ec2-managed-prefix-list", "directconnect-direct-connect-gateway"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_ec2_transit_gateway_route.id"}, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, +}) diff --git a/aws-source/adapters/ec2-transit-gateway-route_test.go b/aws-source/adapters/ec2-transit-gateway-route_test.go new file mode 100644 index 00000000..888edb22 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route_test.go @@ -0,0 +1,68 @@ +package adapters + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestTransitGatewayRouteDestination(t *testing.T) { + if transitGatewayRouteDestination(&types.TransitGatewayRoute{DestinationCidrBlock: PtrString("10.0.0.0/16")}) != "10.0.0.0/16" { + t.Error("expected CIDR destination") + } + if transitGatewayRouteDestination(&types.TransitGatewayRoute{PrefixListId: PtrString("pl-123")}) != "pl:pl-123" { + t.Error("expected prefix list destination") + } +} + +func TestParseRouteQuery(t *testing.T) { + rt, dest, err := parseRouteQuery("tgw-rtb-1|10.0.0.0/16") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || dest != "10.0.0.0/16" { + t.Errorf("expected tgw-rtb-1, 10.0.0.0/16 got %q, %q", rt, dest) + } + // Terraform uses underscore as separator + rt, dest, err = parseRouteQuery("tgw-rtb-1_10.0.0.0/16") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || dest != "10.0.0.0/16" { + t.Errorf("expected tgw-rtb-1, 10.0.0.0/16 (underscore) got %q, %q", rt, dest) + } + _, _, err = parseRouteQuery("bad") + if err == nil { + t.Error("expected error for bad query") + } +} + +func TestTransitGatewayRouteItemMapper(t *testing.T) { + item := &transitGatewayRouteItem{ + RouteTableID: "tgw-rtb-123", + Route: types.TransitGatewayRoute{ + DestinationCidrBlock: PtrString("10.0.0.0/16"), + State: types.TransitGatewayRouteStateActive, + Type: types.TransitGatewayRouteTypeStatic, + }, + } + sdpItem, err := transitGatewayRouteItemMapper("", "account|region", item) + if err != nil { + t.Fatal(err) + } + if err := sdpItem.Validate(); err != nil { + t.Error(err) + } + if sdpItem.GetType() != "ec2-transit-gateway-route" { + t.Errorf("unexpected type %s", sdpItem.GetType()) + } +} + +func TestNewEC2TransitGatewayRouteAdapter(t *testing.T) { + client, account, region := ec2GetAutoConfig(t) + adapter := NewEC2TransitGatewayRouteAdapter(client, account, region, sdpcache.NewNoOpCache()) + if err := adapter.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/aws-source/adapters/ec2-volume-status.go b/aws-source/adapters/ec2-volume-status.go index a8db5d2c..e2eec450 100644 --- a/aws-source/adapters/ec2-volume-status.go +++ b/aws-source/adapters/ec2-volume-status.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func volumeStatusInputMapperGet(scope string, query string) (*ec2.DescribeVolumeStatusInput, error) { @@ -52,11 +52,6 @@ func volumeStatusOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ Query: *volume.VolumeId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Volume and status are tightly coupled - In: true, - Out: true, - }, }, }, } @@ -83,11 +78,6 @@ func volumeStatusOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ Query: *event.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Instances and volumes can affect each other - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-volume-status_test.go b/aws-source/adapters/ec2-volume-status_test.go index 7c01e8e3..a5ac6cae 100644 --- a/aws-source/adapters/ec2-volume-status_test.go +++ b/aws-source/adapters/ec2-volume-status_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVolumeStatusInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-volume.go b/aws-source/adapters/ec2-volume.go index 64351895..469b27ce 100644 --- a/aws-source/adapters/ec2-volume.go +++ b/aws-source/adapters/ec2-volume.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func volumeInputMapperGet(scope string, query string) (*ec2.DescribeVolumesInput, error) { @@ -53,11 +53,6 @@ func volumeOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.D Query: *attachment.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The instance and the volume are closely linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-volume_test.go b/aws-source/adapters/ec2-volume_test.go index 9dd02fcd..277c8dbb 100644 --- a/aws-source/adapters/ec2-volume_test.go +++ b/aws-source/adapters/ec2-volume_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVolumeInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc-endpoint.go b/aws-source/adapters/ec2-vpc-endpoint.go index fab2bff3..96bb7018 100644 --- a/aws-source/adapters/ec2-vpc-endpoint.go +++ b/aws-source/adapters/ec2-vpc-endpoint.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func vpcEndpointInputMapperGet(scope string, query string) (*ec2.DescribeVpcEndpointsInput, error) { @@ -95,11 +95,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: *endpoint.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the VPC overall - In: true, - Out: false, - }, }) } @@ -115,11 +110,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: routeTableID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the route table overall - In: true, - Out: false, - }, }) } @@ -131,11 +121,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: subnetID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the subnet overall - In: true, - Out: false, - }, }) } @@ -148,11 +133,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: *group.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the security group overall - In: true, - Out: false, - }, }) } } @@ -166,11 +146,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: *dnsEntry.DnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } @@ -182,11 +157,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: *dnsEntry.HostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the hosted zone overall - In: true, - Out: false, - }, }) } } @@ -199,11 +169,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: networkInterfaceID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-vpc-endpoint_test.go b/aws-source/adapters/ec2-vpc-endpoint_test.go index 8010afaa..8a471d13 100644 --- a/aws-source/adapters/ec2-vpc-endpoint_test.go +++ b/aws-source/adapters/ec2-vpc-endpoint_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVpcEndpointInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc-peering-connection.go b/aws-source/adapters/ec2-vpc-peering-connection.go index 35969cf2..eccdc459 100644 --- a/aws-source/adapters/ec2-vpc-peering-connection.go +++ b/aws-source/adapters/ec2-vpc-peering-connection.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func vpcPeeringConnectionOutputMapper(_ context.Context, _ *ec2.Client, scope string, input *ec2.DescribeVpcPeeringConnectionsInput, output *ec2.DescribeVpcPeeringConnectionsOutput) ([]*sdp.Item, error) { @@ -63,12 +63,6 @@ func vpcPeeringConnectionOutputMapper(_ context.Context, _ *ec2.Client, scope st Query: *connection.AccepterVpcInfo.VpcId, Scope: pairedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The VPC will affect everything in it - In: true, - // We can't affect the VPC - Out: false, - }, }) } } @@ -87,12 +81,6 @@ func vpcPeeringConnectionOutputMapper(_ context.Context, _ *ec2.Client, scope st Query: *connection.RequesterVpcInfo.VpcId, Scope: pairedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The VPC will affect everything in it - In: true, - // We can't affect the VPC - Out: false, - }, }) } } diff --git a/aws-source/adapters/ec2-vpc-peering-connection_test.go b/aws-source/adapters/ec2-vpc-peering-connection_test.go index 4a31a669..6b2b6da3 100644 --- a/aws-source/adapters/ec2-vpc-peering-connection_test.go +++ b/aws-source/adapters/ec2-vpc-peering-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVpcPeeringConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc.go b/aws-source/adapters/ec2-vpc.go index 89f39dc6..4c7cb989 100644 --- a/aws-source/adapters/ec2-vpc.go +++ b/aws-source/adapters/ec2-vpc.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func vpcInputMapperGet(scope string, query string) (*ec2.DescribeVpcsInput, error) { diff --git a/aws-source/adapters/ec2-vpc_test.go b/aws-source/adapters/ec2-vpc_test.go index edd5cff9..fea243cd 100644 --- a/aws-source/adapters/ec2-vpc_test.go +++ b/aws-source/adapters/ec2-vpc_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVpcInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ecs-capacity-provider.go b/aws-source/adapters/ecs-capacity-provider.go index 88490428..38583089 100644 --- a/aws-source/adapters/ecs-capacity-provider.go +++ b/aws-source/adapters/ecs-capacity-provider.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var CapacityProviderIncludeFields = []types.CapacityProviderField{ @@ -43,11 +43,6 @@ func capacityProviderOutputMapper(_ context.Context, _ ECSClient, scope string, Query: *provider.AutoScalingGroupProvider.AutoScalingGroupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/ecs-capacity-provider_test.go b/aws-source/adapters/ecs-capacity-provider_test.go index faf453a8..77724230 100644 --- a/aws-source/adapters/ecs-capacity-provider_test.go +++ b/aws-source/adapters/ecs-capacity-provider_test.go @@ -7,9 +7,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeCapacityProviders(ctx context.Context, params *ecs.DescribeCapacityProvidersInput, optFns ...func(*ecs.Options)) (*ecs.DescribeCapacityProvidersOutput, error) { diff --git a/aws-source/adapters/ecs-cluster.go b/aws-source/adapters/ecs-cluster.go index 2f957fa5..ba973d2e 100644 --- a/aws-source/adapters/ecs-cluster.go +++ b/aws-source/adapters/ecs-cluster.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // ClusterIncludeFields Fields that we want included by default @@ -75,13 +75,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.ClusterName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Container instances can affect the cluster - In: true, - // The cluster will definitely affect the container - // instances - Out: true, - }, }, { Query: &sdp.Query{ @@ -90,12 +83,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.ClusterName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Services won't affect the cluster - In: false, - // The cluster will definitely affect the services - Out: true, - }, }, { Query: &sdp.Query{ @@ -104,12 +91,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.ClusterName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tasks won't affect the cluster - In: false, - // The cluster will definitely affect the tasks - Out: true, - }, }, }, } @@ -140,12 +121,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.Configuration.ExecuteCommandConfiguration.KmsKeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the KMS key will probably affect the cluster - In: true, - // The cluster won't affect the KMS key though - Out: false, - }, }) } @@ -158,11 +133,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.Configuration.ExecuteCommandConfiguration.LogConfiguration.CloudWatchLogGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } @@ -174,11 +144,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.Configuration.ExecuteCommandConfiguration.LogConfiguration.S3BucketName, Scope: FormatScope(accountID, ""), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -193,11 +158,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: provider, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ecs-cluster_test.go b/aws-source/adapters/ecs-cluster_test.go index ebe86965..3ee97b51 100644 --- a/aws-source/adapters/ecs-cluster_test.go +++ b/aws-source/adapters/ecs-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeClusters(ctx context.Context, params *ecs.DescribeClustersInput, optFns ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error) { diff --git a/aws-source/adapters/ecs-container-instance.go b/aws-source/adapters/ecs-container-instance.go index 3eef002d..021c1a3e 100644 --- a/aws-source/adapters/ecs-container-instance.go +++ b/aws-source/adapters/ecs-container-instance.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // ContainerInstanceIncludeFields Fields that we want included by default @@ -73,11 +73,6 @@ func containerInstanceGetFunc(ctx context.Context, client ECSClient, scope strin Query: *containerInstance.Ec2InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These represent the same thing - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ecs-container-instance_test.go b/aws-source/adapters/ecs-container-instance_test.go index 73fb9818..6a9b8632 100644 --- a/aws-source/adapters/ecs-container-instance_test.go +++ b/aws-source/adapters/ecs-container-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeContainerInstances(ctx context.Context, params *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) { diff --git a/aws-source/adapters/ecs-service.go b/aws-source/adapters/ecs-service.go index f0b5fcc4..fcbbc3ff 100644 --- a/aws-source/adapters/ecs-service.go +++ b/aws-source/adapters/ecs-service.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // ServiceIncludeFields Fields that we want included by default @@ -91,12 +91,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *service.ClusterArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the cluster will affect the service - In: true, - // The service should be able to affect the cluster - Out: false, - }, }) } } @@ -111,11 +105,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *lb.TargetGroupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -131,11 +120,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *sr.RegistryArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -150,12 +134,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *service.TaskDefinition, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the task definition will affect the service - In: true, - // The service shouldn't affect the task definition itself - Out: false, - }, }) } } @@ -170,12 +148,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *deployment.TaskDefinition, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the task definition will affect the service - In: true, - // The service shouldn't affect the task definition itself - Out: false, - }, }) } } @@ -189,12 +161,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *strategy.CapacityProvider, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the capacity provider will affect the service - In: true, - // The service shouldn't affect the capacity provider itself - Out: false, - }, }) } } @@ -209,12 +175,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: subnet, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the service - In: true, - // The service shouldn't affect the subnet - Out: false, - }, }) } @@ -226,12 +186,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: sg, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the service - In: true, - // The service shouldn't affect the security group - Out: false, - }, }) } } @@ -248,11 +202,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *alias.DnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always links - In: true, - Out: true, - }, }) } } @@ -269,11 +218,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *cr.DiscoveryArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -290,12 +234,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: subnet, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the service - In: true, - // The service shouldn't affect the subnet - Out: false, - }, }) } @@ -307,12 +245,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: sg, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the service - In: true, - // The service shouldn't affect the security group - Out: false, - }, }) } } @@ -326,11 +258,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ecs-service_test.go b/aws-source/adapters/ecs-service_test.go index 41b6789d..b7e6683d 100644 --- a/aws-source/adapters/ecs-service_test.go +++ b/aws-source/adapters/ecs-service_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.DescribeServicesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) { diff --git a/aws-source/adapters/ecs-task-definition.go b/aws-source/adapters/ecs-task-definition.go index ba26315d..41e691f6 100644 --- a/aws-source/adapters/ecs-task-definition.go +++ b/aws-source/adapters/ecs-task-definition.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // TaskDefinitionIncludeFields Fields that we want included by default @@ -95,12 +95,6 @@ func taskDefinitionGetFunc(ctx context.Context, client ECSClient, scope string, Query: *td.ExecutionRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The role can affect the task definition - In: true, - // The task definition can't affect the role - Out: false, - }, }) } } @@ -114,12 +108,6 @@ func taskDefinitionGetFunc(ctx context.Context, client ECSClient, scope string, Query: *td.TaskRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The role can affect the task definition - In: true, - // The task definition can't affect the role - Out: false, - }, }) } } @@ -145,12 +133,6 @@ func getSecretLinkedItem(secret types.Secret) *sdp.LinkedItemQuery { Query: *secret.ValueFrom, Scope: secretScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The secret can affect the task definition - In: true, - // The task definition can't affect the secret - Out: false, - }, } case "ssm": return &sdp.LinkedItemQuery{ @@ -160,12 +142,6 @@ func getSecretLinkedItem(secret types.Secret) *sdp.LinkedItemQuery { Query: *secret.ValueFrom, Scope: secretScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The secret can affect the task definition - In: true, - // The task definition can't affect the secret - Out: false, - }, } } } diff --git a/aws-source/adapters/ecs-task-definition_test.go b/aws-source/adapters/ecs-task-definition_test.go index 7f65a16d..4b7ce3cb 100644 --- a/aws-source/adapters/ecs-task-definition_test.go +++ b/aws-source/adapters/ecs-task-definition_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { diff --git a/aws-source/adapters/ecs-task.go b/aws-source/adapters/ecs-task.go index 9632beed..b301f1a7 100644 --- a/aws-source/adapters/ecs-task.go +++ b/aws-source/adapters/ecs-task.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // TaskIncludeFields Fields that we want included by default @@ -79,11 +79,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *attachment.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -99,12 +94,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *task.ClusterArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The cluster can affect the task - In: true, - // The task can't affect the cluster - Out: false, - }, }) } } @@ -118,12 +107,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: a.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The container instance can affect the task - In: true, - // The task can't affect the container instance - Out: false, - }, }) } } @@ -138,11 +121,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *ni.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -154,11 +132,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *ni.PrivateIpv4Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -173,12 +146,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *task.TaskDefinitionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The task definition can affect the task - In: true, - // The task can't affect the task definition - Out: false, - }, }) } } diff --git a/aws-source/adapters/ecs-task_test.go b/aws-source/adapters/ecs-task_test.go index 0f460c8b..e0e8b1b0 100644 --- a/aws-source/adapters/ecs-task_test.go +++ b/aws-source/adapters/ecs-task_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { diff --git a/aws-source/adapters/efs-access-point.go b/aws-source/adapters/efs-access-point.go index ef52ff49..15666b83 100644 --- a/aws-source/adapters/efs-access-point.go +++ b/aws-source/adapters/efs-access-point.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func AccessPointOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeAccessPointsInput, output *efs.DescribeAccessPointsOutput) ([]*sdp.Item, error) { @@ -41,11 +41,6 @@ func AccessPointOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *ap.FileSystemId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Access points are tightly coupled with filesystems - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/efs-access-point_test.go b/aws-source/adapters/efs-access-point_test.go index 9327b0d0..2a47b2a6 100644 --- a/aws-source/adapters/efs-access-point_test.go +++ b/aws-source/adapters/efs-access-point_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAccessPointOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-backup-policy.go b/aws-source/adapters/efs-backup-policy.go index 442aea01..cbb43401 100644 --- a/aws-source/adapters/efs-backup-policy.go +++ b/aws-source/adapters/efs-backup-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func BackupPolicyOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeBackupPolicyInput, output *efs.DescribeBackupPolicyOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-file-system.go b/aws-source/adapters/efs-file-system.go index b6654cce..63fcac44 100644 --- a/aws-source/adapters/efs-file-system.go +++ b/aws-source/adapters/efs-file-system.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeFileSystemsInput, output *efs.DescribeFileSystemsOutput) ([]*sdp.Item, error) { @@ -43,13 +43,6 @@ func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, inpu Query: *fs.FileSystemId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the backup policy could effect the file - // system in that it might no longer be backed up - In: true, - // Changing the file system will not effect the backup - Out: false, - }, }, { Query: &sdp.Query{ @@ -58,11 +51,6 @@ func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, inpu Query: *fs.FileSystemId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - }, }, }, } @@ -77,12 +65,6 @@ func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, inpu Query: *fs.KmsKeyId, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key will affect us - In: true, - // We can't affect the key - Out: false, - }, }) } } diff --git a/aws-source/adapters/efs-file-system_test.go b/aws-source/adapters/efs-file-system_test.go index b0528b98..6b0831dd 100644 --- a/aws-source/adapters/efs-file-system_test.go +++ b/aws-source/adapters/efs-file-system_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestFileSystemOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-mount-target.go b/aws-source/adapters/efs-mount-target.go index 5223e644..67f7374c 100644 --- a/aws-source/adapters/efs-mount-target.go +++ b/aws-source/adapters/efs-mount-target.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeMountTargetsInput, output *efs.DescribeMountTargetsOutput) ([]*sdp.Item, error) { @@ -58,12 +58,6 @@ func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *mt.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the subnet could affect the mount but no the - // other way around - In: true, - Out: false, - }, }) } @@ -75,11 +69,6 @@ func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *mt.IpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always bidirectional - In: true, - Out: true, - }, }) } @@ -91,11 +80,6 @@ func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *mt.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } @@ -107,12 +91,6 @@ func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *mt.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the VPC will affect us - In: true, - // We can't affect the VPC - Out: false, - }, }) } diff --git a/aws-source/adapters/efs-mount-target_test.go b/aws-source/adapters/efs-mount-target_test.go index d5f34d3c..30885f3b 100644 --- a/aws-source/adapters/efs-mount-target_test.go +++ b/aws-source/adapters/efs-mount-target_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestMountTargetOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-replication-configuration.go b/aws-source/adapters/efs-replication-configuration.go index fb151deb..c966ef22 100644 --- a/aws-source/adapters/efs-replication-configuration.go +++ b/aws-source/adapters/efs-replication-configuration.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func ReplicationConfigurationOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeReplicationConfigurationsInput, output *efs.DescribeReplicationConfigurationsOutput) ([]*sdp.Item, error) { @@ -66,12 +66,6 @@ func ReplicationConfigurationOutputMapper(_ context.Context, _ *efs.Client, scop Query: *destination.FileSystemId, Scope: FormatScope(accountID, *destination.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the destination shouldn't affect the source - In: false, - // Changes to this can affect the destination - Out: true, - }, }) } } @@ -107,12 +101,6 @@ func ReplicationConfigurationOutputMapper(_ context.Context, _ *efs.Client, scop Query: *replication.OriginalSourceFileSystemArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the source file system will affect its replication - In: true, - // Changing replication shouldn't affect the filesystem itself - Out: false, - }, }) } diff --git a/aws-source/adapters/efs-replication-configuration_test.go b/aws-source/adapters/efs-replication-configuration_test.go index d56b9f64..d3cf233c 100644 --- a/aws-source/adapters/efs-replication-configuration_test.go +++ b/aws-source/adapters/efs-replication-configuration_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/efs.go b/aws-source/adapters/efs.go index 76e6b20c..c6d9c89e 100644 --- a/aws-source/adapters/efs.go +++ b/aws-source/adapters/efs.go @@ -2,7 +2,7 @@ package adapters import ( "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // lifeCycleStateToHealth Converts a lifecycle state to a health state diff --git a/aws-source/adapters/eks-addon.go b/aws-source/adapters/eks-addon.go index 69da441c..163a1b3a 100644 --- a/aws-source/adapters/eks-addon.go +++ b/aws-source/adapters/eks-addon.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func addonGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeAddonInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-addon_test.go b/aws-source/adapters/eks-addon_test.go index eeb544ea..dacefe45 100644 --- a/aws-source/adapters/eks-addon_test.go +++ b/aws-source/adapters/eks-addon_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var AddonTestClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-cluster.go b/aws-source/adapters/eks-cluster.go index b2f4f33b..078512d2 100644 --- a/aws-source/adapters/eks-cluster.go +++ b/aws-source/adapters/eks-cluster.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeClusterInput) (*sdp.Item, error) { @@ -46,11 +46,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -59,11 +54,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -72,11 +62,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, }, } @@ -108,12 +93,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.ConnectorConfig.RoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The role can affect the cluster - In: true, - // The cluster can't affect the role - Out: false, - }, }) } } @@ -130,12 +109,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *conf.Provider.KeyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The key can affect the cluster - In: true, - // The cluster can't affect the key - Out: false, - }, }) } } @@ -150,11 +123,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.Endpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // HTTP should be linked bidirectionally - In: true, - Out: true, - }, }) } @@ -167,12 +135,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.ResourcesVpcConfig.ClusterSecurityGroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The SG can affect the cluster - In: true, - // The cluster can't affect the SG - Out: false, - }, }) } @@ -184,12 +146,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The SG can affect the cluster - In: true, - // The cluster can't affect the SG - Out: false, - }, }) } @@ -201,12 +157,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The subnet can affect the cluster - In: true, - // The cluster can't affect the subnet - Out: false, - }, }) } @@ -218,12 +168,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.ResourcesVpcConfig.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The VPC can affect the cluster - In: true, - // The cluster can't affect the VPC - Out: false, - }, }) } } @@ -237,12 +181,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.RoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The role can affect the cluster - In: true, - // The cluster can't affect the role - Out: false, - }, }) } } diff --git a/aws-source/adapters/eks-cluster_test.go b/aws-source/adapters/eks-cluster_test.go index 95aa92a0..80dd9d3f 100644 --- a/aws-source/adapters/eks-cluster_test.go +++ b/aws-source/adapters/eks-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var ClusterClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-fargate-profile.go b/aws-source/adapters/eks-fargate-profile.go index 4db6d7f3..b4af7ff4 100644 --- a/aws-source/adapters/eks-fargate-profile.go +++ b/aws-source/adapters/eks-fargate-profile.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func fargateProfileGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeFargateProfileInput) (*sdp.Item, error) { @@ -51,12 +51,6 @@ func fargateProfileGetFunc(ctx context.Context, client EKSClient, scope string, Query: *out.FargateProfile.PodExecutionRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The execution role will affect the fargate profile - In: true, - // The fargate profile can't affect the execution role - Out: false, - }, }) } } @@ -69,12 +63,6 @@ func fargateProfileGetFunc(ctx context.Context, client EKSClient, scope string, Query: subnet, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The subnet will affect the fargate profile - In: true, - // The fargate profile can't affect the subnet - Out: false, - }, }) } diff --git a/aws-source/adapters/eks-fargate-profile_test.go b/aws-source/adapters/eks-fargate-profile_test.go index ab107381..6d6cf7db 100644 --- a/aws-source/adapters/eks-fargate-profile_test.go +++ b/aws-source/adapters/eks-fargate-profile_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var FargateTestClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-nodegroup.go b/aws-source/adapters/eks-nodegroup.go index c340f621..b1a41fa7 100644 --- a/aws-source/adapters/eks-nodegroup.go +++ b/aws-source/adapters/eks-nodegroup.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeNodegroupInput) (*sdp.Item, error) { @@ -65,12 +65,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: *ng.RemoteAccess.Ec2SshKey, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The key pair can affect the node group - In: true, - // The node group can't affect the key pair - Out: false, - }, }) } @@ -82,12 +76,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: sg, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The security group can affect the node group - In: true, - // The node group can't affect the security group - Out: false, - }, }) } } @@ -100,12 +88,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: subnet, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The subnet can affect the node group - In: true, - // The node group can't affect the subnet - Out: false, - }, }) } @@ -119,11 +101,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: *g.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - }, }) } } @@ -136,12 +113,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: *ng.Resources.RemoteAccessSecurityGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The security group can affect the node group - In: true, - // The node group can't affect the security group - Out: false, - }, }) } } @@ -155,12 +126,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: *ng.LaunchTemplate.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The launch template can affect the node group - In: true, - // The node group can't affect the launch template - Out: false, - }, }) } } diff --git a/aws-source/adapters/eks-nodegroup_test.go b/aws-source/adapters/eks-nodegroup_test.go index 2a64ba1d..c630f329 100644 --- a/aws-source/adapters/eks-nodegroup_test.go +++ b/aws-source/adapters/eks-nodegroup_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var NodeGroupClient = EKSTestClient{ diff --git a/aws-source/adapters/elb-instance-health.go b/aws-source/adapters/elb-instance-health.go index 096cc1a2..bc17f5ff 100644 --- a/aws-source/adapters/elb-instance-health.go +++ b/aws-source/adapters/elb-instance-health.go @@ -9,8 +9,8 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // InstanceHealthName Structured representation of an instance health's unique @@ -73,11 +73,6 @@ func instanceHealthOutputMapper(_ context.Context, _ *elb.Client, scope string, Query: *is.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/elb-instance-health_test.go b/aws-source/adapters/elb-instance-health_test.go index 676a04f7..de83a9cc 100644 --- a/aws-source/adapters/elb-instance-health_test.go +++ b/aws-source/adapters/elb-instance-health_test.go @@ -7,7 +7,7 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestInstanceHealthOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elb-load-balancer.go b/aws-source/adapters/elb-load-balancer.go index 1992d468..f7c39e9d 100644 --- a/aws-source/adapters/elb-load-balancer.go +++ b/aws-source/adapters/elb-load-balancer.go @@ -6,8 +6,8 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type elbClient interface { diff --git a/aws-source/adapters/elb-load-balancer_test.go b/aws-source/adapters/elb-load-balancer_test.go index 3700f1e9..e54a56c9 100644 --- a/aws-source/adapters/elb-load-balancer_test.go +++ b/aws-source/adapters/elb-load-balancer_test.go @@ -8,7 +8,7 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type mockElbClient struct{} diff --git a/aws-source/adapters/elbv2-listener.go b/aws-source/adapters/elbv2-listener.go index 6047d67c..a6950275 100644 --- a/aws-source/adapters/elbv2-listener.go +++ b/aws-source/adapters/elbv2-listener.go @@ -8,8 +8,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeListenersInput, output *elbv2.DescribeListenersOutput) ([]*sdp.Item, error) { @@ -74,11 +74,6 @@ func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, Query: *listener.LoadBalancerArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Load balancers and their listeners are tightly coupled - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -88,11 +83,6 @@ func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, Query: *listener.ListenerArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -107,12 +97,6 @@ func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, Query: *cert.CertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the cert will affect the LB - In: true, - // The LB won't affect the cert - Out: false, - }, }) } } diff --git a/aws-source/adapters/elbv2-listener_test.go b/aws-source/adapters/elbv2-listener_test.go index e370da2c..01a3eb6c 100644 --- a/aws-source/adapters/elbv2-listener_test.go +++ b/aws-source/adapters/elbv2-listener_test.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestListenerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-load-balancer.go b/aws-source/adapters/elbv2-load-balancer.go index 59967eb2..dfc3dabe 100644 --- a/aws-source/adapters/elbv2-load-balancer.go +++ b/aws-source/adapters/elbv2-load-balancer.go @@ -5,8 +5,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeLoadBalancersInput, output *elbv2.DescribeLoadBalancersOutput) ([]*sdp.Item, error) { @@ -52,11 +52,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.LoadBalancerArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Load balancers and their target groups are tightly coupled - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -66,11 +61,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.LoadBalancerArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Load balancers and their listeners are tightly coupled - In: true, - Out: true, - }, }) } @@ -82,11 +72,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.DNSName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always links - In: true, - Out: true, - }, }) } @@ -98,12 +83,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.CanonicalHostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone could affect the LB - In: true, - // The LB won't affect the hosted zone - Out: false, - }, }) } @@ -115,12 +94,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC could affect the LB - In: true, - // The LB won't affect the VPC - Out: false, - }, }) } @@ -133,12 +106,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *az.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet could affect the LB - In: true, - // The LB won't affect the subnet - Out: false, - }, }) } @@ -151,12 +118,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *address.AllocationId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the address could affect the LB - In: true, - // The LB can also affect the address - Out: true, - }, }) } @@ -168,11 +129,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *address.IPv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } @@ -184,11 +140,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *address.IpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } @@ -200,11 +151,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *address.PrivateIPv4Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } } @@ -218,12 +164,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: sg, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group could affect the LB - In: true, - // The LB won't affect the security group - Out: false, - }, }) } @@ -235,12 +175,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.CustomerOwnedIpv4Pool, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the COIP pool could affect the LB - In: true, - // The LB won't affect the COIP pool - Out: false, - }, }) } diff --git a/aws-source/adapters/elbv2-load-balancer_test.go b/aws-source/adapters/elbv2-load-balancer_test.go index 05dd7c62..ed37a0a7 100644 --- a/aws-source/adapters/elbv2-load-balancer_test.go +++ b/aws-source/adapters/elbv2-load-balancer_test.go @@ -8,7 +8,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestLoadBalancerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-rule.go b/aws-source/adapters/elbv2-rule.go index 99aa3795..14975ff9 100644 --- a/aws-source/adapters/elbv2-rule.go +++ b/aws-source/adapters/elbv2-rule.go @@ -5,8 +5,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func ruleOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeRulesInput, output *elbv2.DescribeRulesOutput) ([]*sdp.Item, error) { @@ -60,11 +60,6 @@ func ruleOutputMapper(ctx context.Context, client elbv2Client, scope string, _ * Query: value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/elbv2-rule_test.go b/aws-source/adapters/elbv2-rule_test.go index 6117bb2c..1cdd7024 100644 --- a/aws-source/adapters/elbv2-rule_test.go +++ b/aws-source/adapters/elbv2-rule_test.go @@ -9,9 +9,9 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestRuleOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-target-group.go b/aws-source/adapters/elbv2-target-group.go index bc4fca11..09af3e31 100644 --- a/aws-source/adapters/elbv2-target-group.go +++ b/aws-source/adapters/elbv2-target-group.go @@ -6,8 +6,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeTargetGroupsInput, output *elbv2.DescribeTargetGroupsOutput) ([]*sdp.Item, error) { @@ -52,11 +52,6 @@ func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope stri Query: *tg.TargetGroupArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Target groups and their target health are tightly coupled - In: true, - Out: true, - }, }) } @@ -68,12 +63,6 @@ func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope stri Query: *tg.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC can affect the target group - In: true, - // The target group won't affect the VPC - Out: false, - }, }) } @@ -86,11 +75,6 @@ func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope stri Query: lbArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Load balancers and their target groups are tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/elbv2-target-group_test.go b/aws-source/adapters/elbv2-target-group_test.go index 88a48ab7..44ec62ba 100644 --- a/aws-source/adapters/elbv2-target-group_test.go +++ b/aws-source/adapters/elbv2-target-group_test.go @@ -8,8 +8,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestTargetGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-target-health.go b/aws-source/adapters/elbv2-target-health.go index 783100aa..ce68427d 100644 --- a/aws-source/adapters/elbv2-target-health.go +++ b/aws-source/adapters/elbv2-target-health.go @@ -10,8 +10,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type TargetHealthUniqueID struct { @@ -152,11 +152,6 @@ func targetHealthOutputMapper(_ context.Context, _ *elbv2.Client, scope string, Query: *desc.Target.Id, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Everything is tightly coupled with target health - In: true, - Out: true, - }, }) case "elasticloadbalancing": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -166,10 +161,6 @@ func targetHealthOutputMapper(_ context.Context, _ *elbv2.Client, scope string, Query: *desc.Target.Id, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } else { @@ -184,10 +175,6 @@ func targetHealthOutputMapper(_ context.Context, _ *elbv2.Client, scope string, Query: *desc.Target.Id, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } else { // If all else fails it must be an instance ID @@ -198,10 +185,6 @@ func targetHealthOutputMapper(_ context.Context, _ *elbv2.Client, scope string, Query: *desc.Target.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/elbv2-target-health_test.go b/aws-source/adapters/elbv2-target-health_test.go index c4b8171b..c15e8a62 100644 --- a/aws-source/adapters/elbv2-target-health_test.go +++ b/aws-source/adapters/elbv2-target-health_test.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTargetHealthOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2.go b/aws-source/adapters/elbv2.go index a045b1af..f214aefa 100644 --- a/aws-source/adapters/elbv2.go +++ b/aws-source/adapters/elbv2.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type elbv2Client interface { @@ -72,12 +72,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.AuthenticateCognitoConfig.UserPoolArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the user pool could affect the LB - In: true, - // The LB won't affect the user pool - Out: false, - }, }) } } @@ -92,12 +86,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.AuthenticateOidcConfig.AuthorizationEndpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the authorization endpoint could affect the LB - In: true, - // The LB won't affect the authorization endpoint - Out: false, - }, }) } @@ -109,12 +97,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.AuthenticateOidcConfig.TokenEndpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the authorization endpoint could affect the LB - In: true, - // The LB won't affect the authorization endpoint - Out: false, - }, }) } @@ -126,12 +108,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.AuthenticateOidcConfig.UserInfoEndpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the authorization endpoint could affect the LB - In: true, - // The LB won't affect the authorization endpoint - Out: false, - }, }) } } @@ -147,12 +123,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *tg.TargetGroupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the target group could affect the LB - In: true, - // The LB could also affect the target group - Out: true, - }, }) } } @@ -194,11 +164,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: u.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These are closely linked - In: true, - Out: true, - }, }) } } @@ -212,11 +177,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.TargetGroupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are closely linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/elbv2_test.go b/aws-source/adapters/elbv2_test.go index 10926076..6079768a 100644 --- a/aws-source/adapters/elbv2_test.go +++ b/aws-source/adapters/elbv2_test.go @@ -6,7 +6,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type mockElbv2Client struct{} diff --git a/aws-source/adapters/iam-group.go b/aws-source/adapters/iam-group.go index 99dbcea5..966eae40 100644 --- a/aws-source/adapters/iam-group.go +++ b/aws-source/adapters/iam-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func groupGetFunc(ctx context.Context, client *iam.Client, _, query string) (*types.Group, error) { diff --git a/aws-source/adapters/iam-group_test.go b/aws-source/adapters/iam-group_test.go index ec6651fe..2ad67320 100644 --- a/aws-source/adapters/iam-group_test.go +++ b/aws-source/adapters/iam-group_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestGroupItemMapper(t *testing.T) { diff --git a/aws-source/adapters/iam-instance-profile.go b/aws-source/adapters/iam-instance-profile.go index 7ca23edc..5c26f9c8 100644 --- a/aws-source/adapters/iam-instance-profile.go +++ b/aws-source/adapters/iam-instance-profile.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func instanceProfileGetFunc(ctx context.Context, client *iam.Client, _, query string) (*types.InstanceProfile, error) { @@ -46,12 +46,6 @@ func instanceProfileItemMapper(_ *string, scope string, awsItem *types.InstanceP Query: *role.Arn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the role will affect this - In: true, - // We can't affect the role - Out: false, - }, }) } @@ -64,12 +58,6 @@ func instanceProfileItemMapper(_ *string, scope string, awsItem *types.InstanceP Query: *role.PermissionsBoundary.PermissionsBoundaryArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the policy will affect this - In: true, - // We can't affect the policy - Out: false, - }, }) } } diff --git a/aws-source/adapters/iam-instance-profile_test.go b/aws-source/adapters/iam-instance-profile_test.go index 092e9d58..50ade39e 100644 --- a/aws-source/adapters/iam-instance-profile_test.go +++ b/aws-source/adapters/iam-instance-profile_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInstanceProfileItemMapper(t *testing.T) { diff --git a/aws-source/adapters/iam-policy.go b/aws-source/adapters/iam-policy.go index 009cb900..5a48ad15 100644 --- a/aws-source/adapters/iam-policy.go +++ b/aws-source/adapters/iam-policy.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/iter" "go.opentelemetry.io/otel/trace" @@ -172,12 +172,6 @@ func policyItemMapper(_ *string, scope string, awsItem *PolicyDetails) (*sdp.Ite Method: sdp.QueryMethod_GET, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the group won't affect the policy - In: false, - // Changing the policy will affect the group - Out: true, - }, }) } @@ -189,12 +183,6 @@ func policyItemMapper(_ *string, scope string, awsItem *PolicyDetails) (*sdp.Ite Query: *user.UserName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the user won't affect the policy - In: false, - // Changing the policy will affect the user - Out: true, - }, }) } @@ -206,12 +194,6 @@ func policyItemMapper(_ *string, scope string, awsItem *PolicyDetails) (*sdp.Ite Query: *role.RoleName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the role won't affect the policy - In: false, - // Changing the policy will affect the role - Out: true, - }, }) } diff --git a/aws-source/adapters/iam-policy_test.go b/aws-source/adapters/iam-policy_test.go index cbdf09cc..a9b85490 100644 --- a/aws-source/adapters/iam-policy_test.go +++ b/aws-source/adapters/iam-policy_test.go @@ -9,9 +9,9 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *TestIAMClient) GetPolicy(ctx context.Context, params *iam.GetPolicyInput, optFns ...func(*iam.Options)) (*iam.GetPolicyOutput, error) { diff --git a/aws-source/adapters/iam-role.go b/aws-source/adapters/iam-role.go index 58d735ac..515f389a 100644 --- a/aws-source/adapters/iam-role.go +++ b/aws-source/adapters/iam-role.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/sourcegraph/conc/iter" ) @@ -201,12 +201,6 @@ func roleItemMapper(_ *string, scope string, awsItem *RoleDetails) (*sdp.Item, e Query: *policy.PolicyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the role - In: true, - // Changing the role won't affect the policy - Out: false, - }, }) } } diff --git a/aws-source/adapters/iam-role_test.go b/aws-source/adapters/iam-role_test.go index 8b581863..e08d1456 100644 --- a/aws-source/adapters/iam-role_test.go +++ b/aws-source/adapters/iam-role_test.go @@ -11,9 +11,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *TestIAMClient) GetRole(ctx context.Context, params *iam.GetRoleInput, optFns ...func(*iam.Options)) (*iam.GetRoleOutput, error) { diff --git a/aws-source/adapters/iam-user.go b/aws-source/adapters/iam-user.go index c96801f2..3afd374c 100644 --- a/aws-source/adapters/iam-user.go +++ b/aws-source/adapters/iam-user.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type UserDetails struct { @@ -99,12 +99,6 @@ func userItemMapper(_ *string, scope string, awsItem *UserDetails) (*sdp.Item, e Query: *group.GroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the group can affect the user - In: true, - // Changing the user won't affect the group - Out: false, - }, }) } diff --git a/aws-source/adapters/iam-user_test.go b/aws-source/adapters/iam-user_test.go index 3d3bbe71..66c6c8af 100644 --- a/aws-source/adapters/iam-user_test.go +++ b/aws-source/adapters/iam-user_test.go @@ -11,9 +11,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *TestIAMClient) ListGroupsForUser(ctx context.Context, params *iam.ListGroupsForUserInput, optFns ...func(*iam.Options)) (*iam.ListGroupsForUserOutput, error) { diff --git a/aws-source/adapters/iam.go b/aws-source/adapters/iam.go index 230a5011..94744a03 100644 --- a/aws-source/adapters/iam.go +++ b/aws-source/adapters/iam.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type IAMClient interface { @@ -69,10 +69,6 @@ var ssmQueryExtractor = QueryExtractor{ Query: a.String() + "*", // Wildcard at the end Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } }, @@ -118,10 +114,6 @@ var fallbackQueryExtractor = QueryExtractor{ Query: arn.String(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } }, @@ -171,13 +163,6 @@ func LinksFromPolicy(document *policy.Policy) []*sdp.LinkedItemQuery { Query: arn.String(), Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // If a user or role iex explicitly - // referenced, I think it's reasonable to - // assume that they are tightly bound - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/iam_test.go b/aws-source/adapters/iam_test.go index 889a91cc..95a2aa25 100644 --- a/aws-source/adapters/iam_test.go +++ b/aws-source/adapters/iam_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) // TestIAMClient Test client that returns three pages diff --git a/aws-source/adapters/integration/apigateway/apigateway_test.go b/aws-source/adapters/integration/apigateway/apigateway_test.go index 25211873..79a7b7c4 100644 --- a/aws-source/adapters/integration/apigateway/apigateway_test.go +++ b/aws-source/adapters/integration/apigateway/apigateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func APIGateway(t *testing.T) { diff --git a/aws-source/adapters/integration/ec2-transit-gateway/client.go b/aws-source/adapters/integration/ec2-transit-gateway/client.go new file mode 100644 index 00000000..228bb164 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/client.go @@ -0,0 +1,17 @@ +package ec2transitgateway + +import ( + "context" + "fmt" + + awsec2 "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/overmindtech/cli/aws-source/adapters/integration" +) + +func ec2Client(ctx context.Context) (*awsec2.Client, error) { + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get AWS settings: %w", err) + } + return awsec2.NewFromConfig(testAWSConfig.Config), nil +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/main_test.go b/aws-source/adapters/integration/ec2-transit-gateway/main_test.go new file mode 100644 index 00000000..7a267186 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/main_test.go @@ -0,0 +1,95 @@ +// Package ec2transitgateway runs integration tests for EC2 Transit Gateway adapters +// (transit gateway route table, route table association, route table propagation, +// and route). Setup creates a transit gateway, VPC, subnet, TGW VPC attachment, +// and a static route so each adapter returns items; Teardown deletes them in order. +// +// All created resources are tagged with name and test-id "integration-test" so they +// are easy to spot in the console and so Teardown can discover them by tag. You can +// run Setup once, re-run the test subtests as needed, then run Teardown once; or run +// Teardown alone to clean up any stale resources from a previous run. +// +// Run integration tests only when RUN_INTEGRATION_TESTS=true. Example CLI commands: +// +// # Setup only (create resources) +// RUN_INTEGRATION_TESTS=true go test ./aws-source/adapters/integration/ec2-transit-gateway -v -count=1 -run '^TestIntegrationEC2TransitGateway$/Setup$' +// +// # Teardown only (delete resources by tag; idempotent) +// RUN_INTEGRATION_TESTS=true go test ./aws-source/adapters/integration/ec2-transit-gateway -v -count=1 -run '^TestIntegrationEC2TransitGateway$/Teardown$' +// +// # Run a single adapter test (e.g. after Setup, re-run as needed) +// RUN_INTEGRATION_TESTS=true go test ./aws-source/adapters/integration/ec2-transit-gateway -v -count=1 -run '^TestIntegrationEC2TransitGateway$/TransitGatewayRouteTable$' +// +// # Run the full suite (Setup, all adapter tests, Teardown) +// RUN_INTEGRATION_TESTS=true go test ./aws-source/adapters/integration/ec2-transit-gateway -v -count=1 -run '^TestIntegrationEC2TransitGateway$' +// +// Cost: a few cents per run. Setup creates a Transit Gateway, a VPC, a subnet, and +// one TGW VPC attachment so that association, propagation, and route adapters +// return items. AWS charges for the TGW and ~$0.05/hour per VPC attachment; with +// teardown within minutes, cost remains low. See https://aws.amazon.com/transit-gateway/pricing/ +// +// Per-adapter cost: route table, association, propagation, and route tests do not +// create additional resources; they list/get from the same TGW and its default +// route table (one attachment, one static route), so they add no extra cost. +// +// To inspect the infrastructure created by the tests: +// +// - AWS CLI (replace [REGION] and [ROUTE_TABLE_ID] as needed): +// +// aws ec2 describe-transit-gateways [--region [REGION]] +// aws ec2 describe-transit-gateway-route-tables [--region [REGION]] +// aws ec2 get-transit-gateway-route-table-associations --transit-gateway-route-table-id [ROUTE_TABLE_ID] [--region [REGION]] +// aws ec2 get-transit-gateway-route-table-propagations --transit-gateway-route-table-id [ROUTE_TABLE_ID] [--region [REGION]] +// aws ec2 search-transit-gateway-routes --transit-gateway-route-table-id [ROUTE_TABLE_ID] --filters "Name=state,Values=active,blackhole" [--region [REGION]] +// +// - AWS Console: EC2 → Network & Security → Transit gateways → select a transit gateway +// `https://eu-west-2.console.aws.amazon.com/vpcconsole/home?region=eu-west-2#TransitGateways:` other resources are displayed on the left hand pane. +package ec2transitgateway + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" +) + +func TestMain(m *testing.M) { + if integration.ShouldRunIntegrationTests() { + fmt.Println("Running EC2 Transit Gateway integration tests") + os.Exit(m.Run()) + } else { + fmt.Println("Skipping EC2 Transit Gateway integration tests, set RUN_INTEGRATION_TESTS=true to run them") + os.Exit(0) + } +} + +func TestIntegrationEC2TransitGateway(t *testing.T) { + // Setup creates resources tagged integration-test; Teardown is idempotent and discovers by tag. + t.Run("Setup", Setup) + t.Run("TransitGatewayRouteTable", TransitGatewayRouteTable) + t.Run("TransitGatewayRouteTableAssociation", TransitGatewayRouteTableAssociation) + t.Run("TransitGatewayRouteTablePropagation", TransitGatewayRouteTablePropagation) + t.Run("TransitGatewayRoute", TransitGatewayRoute) + t.Run("Teardown", Teardown) +} + +func listSync(adapter discovery.ListStreamableAdapter, ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { + stream := discovery.NewRecordingQueryResultStream() + adapter.ListStream(ctx, scope, ignoreCache, stream) + if errs := stream.GetErrors(); len(errs) > 0 { + return nil, fmt.Errorf("failed to list: %v", errs) + } + return stream.GetItems(), nil +} + +func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { + stream := discovery.NewRecordingQueryResultStream() + adapter.SearchStream(ctx, scope, query, ignoreCache, stream) + if errs := stream.GetErrors(); len(errs) > 0 { + return nil, fmt.Errorf("failed to search: %v", errs) + } + return stream.GetItems(), nil +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/setup.go b/aws-source/adapters/integration/ec2-transit-gateway/setup.go new file mode 100644 index 00000000..ec86afd7 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/setup.go @@ -0,0 +1,243 @@ +package ec2transitgateway + +import ( + "context" + "errors" + "log/slog" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/aws-source/adapters/integration" +) + +// integrationTestName is the fixed tag value and name for all resources created by +// this suite. Teardown discovers and deletes resources by tag test-id=, so +// it can be run alone to clean stale resources from previous runs. +const integrationTestName = "integration-test" + +// Package-level state set by Setup and used by tests and Teardown. +var ( + createdTransitGatewayID string + createdRouteTableID string + createdVpcID string + createdSubnetID string + createdAttachmentID string + createdRouteDestination = "10.88.0.0/16" // static route we create (distinct from VPC CIDR) +) + +func Setup(t *testing.T) { + ctx := context.Background() + logger := slog.Default() + + client, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + if err := setup(ctx, logger, client); err != nil { + t.Fatalf("Setup failed: %v", err) + } +} + +func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { + out, err := client.CreateTransitGateway(ctx, &ec2.CreateTransitGatewayInput{ + Description: ptr("Overmind " + integrationTestName), + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeTransitGateway, + Tags: []types.Tag{ + {Key: ptr(integration.TagTestKey), Value: ptr(integration.TagTestValue)}, + {Key: ptr(integration.TagTestIDKey), Value: ptr(integrationTestName)}, + {Key: ptr("Name"), Value: ptr(integrationTestName)}, + }, + }, + }, + }) + if err != nil { + return err + } + + if out.TransitGateway == nil || out.TransitGateway.TransitGatewayId == nil { + return errors.New("CreateTransitGateway returned nil transit gateway or id") + } + + tgwID := *out.TransitGateway.TransitGatewayId + createdTransitGatewayID = tgwID + logger.InfoContext(ctx, "Created transit gateway, waiting for available", "id", tgwID) + + // Wait for transit gateway to become available (creates default route table). + const waitTimeout = 5 * time.Minute + deadline := time.Now().Add(waitTimeout) + tgwAvailable := false + for time.Now().Before(deadline) { + desc, err := client.DescribeTransitGateways(ctx, &ec2.DescribeTransitGatewaysInput{ + TransitGatewayIds: []string{tgwID}, + }) + if err != nil { + return err + } + if len(desc.TransitGateways) == 0 { + time.Sleep(10 * time.Second) + continue + } + state := desc.TransitGateways[0].State + if state == types.TransitGatewayStateAvailable { + tgwAvailable = true + break + } + if state == types.TransitGatewayStateDeleted || state == types.TransitGatewayStateDeleting { + return errors.New("transit gateway entered deleted/deleting state") + } + time.Sleep(10 * time.Second) + } + if !tgwAvailable { + return errors.New("timeout waiting for transit gateway to become available") + } + + // Resolve default route table for this TGW (needed for attachment and static route). + rtOut, err := client.DescribeTransitGatewayRouteTables(ctx, &ec2.DescribeTransitGatewayRouteTablesInput{ + Filters: []types.Filter{ + {Name: ptr("transit-gateway-id"), Values: []string{tgwID}}, + }, + }) + if err != nil { + return err + } + for i := range rtOut.TransitGatewayRouteTables { + rt := &rtOut.TransitGatewayRouteTables[i] + if rt.TransitGatewayRouteTableId != nil && rt.DefaultAssociationRouteTable != nil && *rt.DefaultAssociationRouteTable { + createdRouteTableID = *rt.TransitGatewayRouteTableId + break + } + } + if createdRouteTableID == "" { + return errors.New("could not find default route table for transit gateway") + } + + // Create VPC and subnet so we can create a VPC attachment (association + propagation + route target). + vpcOut, err := client.CreateVpc(ctx, &ec2.CreateVpcInput{ + CidrBlock: ptr("10.99.0.0/16"), + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeVpc, + Tags: []types.Tag{ + {Key: ptr(integration.TagTestKey), Value: ptr(integration.TagTestValue)}, + {Key: ptr(integration.TagTestIDKey), Value: ptr(integrationTestName)}, + {Key: ptr("Name"), Value: ptr(integrationTestName)}, + }, + }, + }, + }) + if err != nil { + return err + } + if vpcOut.Vpc == nil || vpcOut.Vpc.VpcId == nil { + return errors.New("CreateVpc returned nil vpc or id") + } + createdVpcID = *vpcOut.Vpc.VpcId + logger.InfoContext(ctx, "Created VPC for TGW attachment", "id", createdVpcID) + + // Pick one AZ for the subnet. + azOut, err := client.DescribeAvailabilityZones(ctx, &ec2.DescribeAvailabilityZonesInput{ + Filters: []types.Filter{ + {Name: ptr("state"), Values: []string{"available"}}, + }, + }) + if err != nil || len(azOut.AvailabilityZones) == 0 { + return errors.New("could not describe availability zones") + } + az := azOut.AvailabilityZones[0].ZoneName + + subOut, err := client.CreateSubnet(ctx, &ec2.CreateSubnetInput{ + VpcId: &createdVpcID, + CidrBlock: ptr("10.99.1.0/24"), + AvailabilityZone: az, + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeSubnet, + Tags: []types.Tag{ + {Key: ptr(integration.TagTestKey), Value: ptr(integration.TagTestValue)}, + {Key: ptr(integration.TagTestIDKey), Value: ptr(integrationTestName)}, + {Key: ptr("Name"), Value: ptr(integrationTestName)}, + }, + }, + }, + }) + if err != nil { + return err + } + if subOut.Subnet == nil || subOut.Subnet.SubnetId == nil { + return errors.New("CreateSubnet returned nil subnet or id") + } + createdSubnetID = *subOut.Subnet.SubnetId + logger.InfoContext(ctx, "Created subnet for TGW attachment", "id", createdSubnetID) + + attachOut, err := client.CreateTransitGatewayVpcAttachment(ctx, &ec2.CreateTransitGatewayVpcAttachmentInput{ + TransitGatewayId: &tgwID, + VpcId: &createdVpcID, + SubnetIds: []string{createdSubnetID}, + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeTransitGatewayAttachment, + Tags: []types.Tag{ + {Key: ptr(integration.TagTestKey), Value: ptr(integration.TagTestValue)}, + {Key: ptr(integration.TagTestIDKey), Value: ptr(integrationTestName)}, + {Key: ptr("Name"), Value: ptr(integrationTestName)}, + }, + }, + }, + }) + if err != nil { + return err + } + if attachOut.TransitGatewayVpcAttachment == nil || attachOut.TransitGatewayVpcAttachment.TransitGatewayAttachmentId == nil { + return errors.New("CreateTransitGatewayVpcAttachment returned nil attachment or id") + } + createdAttachmentID = *attachOut.TransitGatewayVpcAttachment.TransitGatewayAttachmentId + logger.InfoContext(ctx, "Created TGW VPC attachment, waiting for available", "id", createdAttachmentID) + + // Wait for attachment to become available so we can create a route and so associations/propagations appear. + attachDeadline := time.Now().Add(waitTimeout) + attachmentAvailable := false + for time.Now().Before(attachDeadline) { + desc, err := client.DescribeTransitGatewayVpcAttachments(ctx, &ec2.DescribeTransitGatewayVpcAttachmentsInput{ + TransitGatewayAttachmentIds: []string{createdAttachmentID}, + }) + if err != nil { + return err + } + if len(desc.TransitGatewayVpcAttachments) == 0 { + time.Sleep(10 * time.Second) + continue + } + state := desc.TransitGatewayVpcAttachments[0].State + if state == types.TransitGatewayAttachmentStateAvailable { + attachmentAvailable = true + break + } + if state == types.TransitGatewayAttachmentStateDeleted || state == types.TransitGatewayAttachmentStateDeleting { + return errors.New("transit gateway VPC attachment entered deleted/deleting state") + } + time.Sleep(10 * time.Second) + } + if !attachmentAvailable { + return errors.New("timeout waiting for transit gateway VPC attachment to become available") + } + + // Add a static route so the route adapter returns at least one item. + _, err = client.CreateTransitGatewayRoute(ctx, &ec2.CreateTransitGatewayRouteInput{ + TransitGatewayRouteTableId: &createdRouteTableID, + DestinationCidrBlock: &createdRouteDestination, + TransitGatewayAttachmentId: &createdAttachmentID, + }) + if err != nil { + return err + } + logger.InfoContext(ctx, "Created static TGW route", "destination", createdRouteDestination) + + return nil +} + +func ptr(s string) *string { return &s } diff --git a/aws-source/adapters/integration/ec2-transit-gateway/teardown.go b/aws-source/adapters/integration/ec2-transit-gateway/teardown.go new file mode 100644 index 00000000..61e94270 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/teardown.go @@ -0,0 +1,181 @@ +package ec2transitgateway + +import ( + "context" + "errors" + "log/slog" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/aws-source/adapters/integration" +) + +// integrationTestTagFilters returns filters to discover resources created by this suite. +func integrationTestTagFilters() []types.Filter { + return []types.Filter{ + {Name: ptr("tag:" + integration.TagTestKey), Values: []string{integration.TagTestValue}}, + {Name: ptr("tag:" + integration.TagTestIDKey), Values: []string{integrationTestName}}, + } +} + +// getIntegrationTestTransitGatewayID returns the transit gateway ID for the integration-test +// resources. If Setup ran in this process, it uses the package-level ID; otherwise it +// discovers the TGW by tag so tests work when run after a separate Setup (e.g. a day later). +// Returns an error if no tagged TGW is found (e.g. after Teardown). +func getIntegrationTestTransitGatewayID(ctx context.Context, client *ec2.Client) (string, error) { + if createdTransitGatewayID != "" { + return createdTransitGatewayID, nil + } + tgwOut, err := client.DescribeTransitGateways(ctx, &ec2.DescribeTransitGatewaysInput{ + Filters: integrationTestTagFilters(), + }) + if err != nil { + return "", err + } + for _, tgw := range tgwOut.TransitGateways { + if tgw.TransitGatewayId != nil && tgw.State != types.TransitGatewayStateDeleted && tgw.State != types.TransitGatewayStateDeleting { + return *tgw.TransitGatewayId, nil + } + } + return "", errors.New("no transit gateway found with integration-test tag (run Setup first or ensure Teardown has not deleted resources)") +} + +func Teardown(t *testing.T) { + ctx := context.Background() + logger := slog.Default() + + client, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + if err := teardown(ctx, logger, client); err != nil { + t.Fatalf("Teardown failed: %v", err) + } +} + +func teardown(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { + tagFilters := integrationTestTagFilters() + + // 1. Discover transit gateways by tag. + tgwOut, err := client.DescribeTransitGateways(ctx, &ec2.DescribeTransitGatewaysInput{ + Filters: tagFilters, + }) + if err != nil { + return err + } + if len(tgwOut.TransitGateways) == 0 { + logger.InfoContext(ctx, "No transit gateways found with integration-test tag") + clearPackageState() + return nil + } + + // 2. For each TGW: delete static route, then VPC attachments, and wait for attachments to be deleted. + for _, tgw := range tgwOut.TransitGateways { + if tgw.TransitGatewayId == nil || tgw.State == types.TransitGatewayStateDeleted || tgw.State == types.TransitGatewayStateDeleting { + continue + } + tgwID := *tgw.TransitGatewayId + + // Resolve default route table and delete our static route. + rtOut, err := client.DescribeTransitGatewayRouteTables(ctx, &ec2.DescribeTransitGatewayRouteTablesInput{ + Filters: []types.Filter{{Name: ptr("transit-gateway-id"), Values: []string{tgwID}}}, + }) + if err != nil { + return err + } + var defaultRouteTableID string + for i := range rtOut.TransitGatewayRouteTables { + rt := &rtOut.TransitGatewayRouteTables[i] + if rt.TransitGatewayRouteTableId != nil && rt.DefaultAssociationRouteTable != nil && *rt.DefaultAssociationRouteTable { + defaultRouteTableID = *rt.TransitGatewayRouteTableId + break + } + } + if defaultRouteTableID != "" { + _, _ = client.DeleteTransitGatewayRoute(ctx, &ec2.DeleteTransitGatewayRouteInput{ + TransitGatewayRouteTableId: &defaultRouteTableID, + DestinationCidrBlock: &createdRouteDestination, + }) + } + + // List VPC attachments for this TGW and delete each. + attachOut, err := client.DescribeTransitGatewayVpcAttachments(ctx, &ec2.DescribeTransitGatewayVpcAttachmentsInput{ + Filters: []types.Filter{{Name: ptr("transit-gateway-id"), Values: []string{tgwID}}}, + }) + if err != nil { + return err + } + for _, att := range attachOut.TransitGatewayVpcAttachments { + if att.TransitGatewayAttachmentId == nil || att.State == types.TransitGatewayAttachmentStateDeleted || att.State == types.TransitGatewayAttachmentStateDeleting { + continue + } + attID := *att.TransitGatewayAttachmentId + _, _ = client.DeleteTransitGatewayVpcAttachment(ctx, &ec2.DeleteTransitGatewayVpcAttachmentInput{ + TransitGatewayAttachmentId: &attID, + }) + logger.InfoContext(ctx, "Deleted TGW VPC attachment, waiting for deleted", "id", attID) + deadline := time.Now().Add(5 * time.Minute) + for time.Now().Before(deadline) { + desc, err := client.DescribeTransitGatewayVpcAttachments(ctx, &ec2.DescribeTransitGatewayVpcAttachmentsInput{ + TransitGatewayAttachmentIds: []string{attID}, + }) + if err != nil || len(desc.TransitGatewayVpcAttachments) == 0 { + break + } + if desc.TransitGatewayVpcAttachments[0].State == types.TransitGatewayAttachmentStateDeleted { + break + } + time.Sleep(10 * time.Second) + } + } + } + + // 3. Delete subnets by tag. + subOut, err := client.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{Filters: tagFilters}) + if err != nil { + return err + } + for _, sub := range subOut.Subnets { + if sub.SubnetId != nil { + _, _ = client.DeleteSubnet(ctx, &ec2.DeleteSubnetInput{SubnetId: sub.SubnetId}) + } + } + + // 4. Delete VPCs by tag. + vpcOut, err := client.DescribeVpcs(ctx, &ec2.DescribeVpcsInput{Filters: tagFilters}) + if err != nil { + return err + } + for _, vpc := range vpcOut.Vpcs { + if vpc.VpcId != nil { + _, _ = client.DeleteVpc(ctx, &ec2.DeleteVpcInput{VpcId: vpc.VpcId}) + } + } + + // 5. Delete transit gateways. + for _, tgw := range tgwOut.TransitGateways { + if tgw.TransitGatewayId == nil || tgw.State == types.TransitGatewayStateDeleted || tgw.State == types.TransitGatewayStateDeleting { + continue + } + tgwID := *tgw.TransitGatewayId + _, err := client.DeleteTransitGateway(ctx, &ec2.DeleteTransitGatewayInput{TransitGatewayId: &tgwID}) + if err != nil { + return err + } + logger.InfoContext(ctx, "Deleted transit gateway", "id", tgwID) + } + + clearPackageState() + return nil +} + +func clearPackageState() { + createdTransitGatewayID = "" + createdRouteTableID = "" + createdVpcID = "" + createdSubnetID = "" + createdAttachmentID = "" +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_association_test.go b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_association_test.go new file mode 100644 index 00000000..c05194ba --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_association_test.go @@ -0,0 +1,62 @@ +package ec2transitgateway + +import ( + "context" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters" + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/sdpcache" +) + +// TransitGatewayRouteTableAssociation runs the integration test for the route table association adapter. +// Setup creates a TGW VPC attachment, so the default route table has at least one association. +func TransitGatewayRouteTableAssociation(t *testing.T) { + ctx := context.Background() + + testClient, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + t.Fatalf("Failed to get AWS settings: %v", err) + } + + scope := adapters.FormatScope(testAWSConfig.AccountID, testAWSConfig.Region) + adapter := adapters.NewEC2TransitGatewayRouteTableAssociationAdapter(testClient, testAWSConfig.AccountID, testAWSConfig.Region, sdpcache.NewNoOpCache()) + + if err := adapter.Validate(); err != nil { + t.Fatalf("failed to validate adapter: %v", err) + } + + items, err := adapter.List(ctx, scope, true) + if err != nil { + t.Fatalf("failed to list transit gateway route table associations: %v", err) + } + + if len(items) == 0 { + t.Fatalf("expected at least one association (Setup creates a TGW VPC attachment); got 0") + } + + query := items[0].UniqueAttributeValue() + got, err := adapter.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get association %s: %v", query, err) + } + if got.UniqueAttributeValue() != query { + t.Fatalf("expected %s, got %s", query, got.UniqueAttributeValue()) + } + + // Search by route table ID (used by route table → association link). + if createdRouteTableID != "" { + searchItems, err := adapter.Search(ctx, scope, createdRouteTableID, true) + if err != nil { + t.Fatalf("failed to search associations by route table ID %s: %v", createdRouteTableID, err) + } + if len(searchItems) == 0 { + t.Fatalf("expected at least one association for route table %s (Setup creates one); got 0", createdRouteTableID) + } + } +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_propagation_test.go b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_propagation_test.go new file mode 100644 index 00000000..8c62b735 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_propagation_test.go @@ -0,0 +1,62 @@ +package ec2transitgateway + +import ( + "context" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters" + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/sdpcache" +) + +// TransitGatewayRouteTablePropagation runs the integration test for the route table propagation adapter. +// Setup creates a TGW VPC attachment (propagated to the default route table), so we get at least one propagation. +func TransitGatewayRouteTablePropagation(t *testing.T) { + ctx := context.Background() + + testClient, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + t.Fatalf("Failed to get AWS settings: %v", err) + } + + scope := adapters.FormatScope(testAWSConfig.AccountID, testAWSConfig.Region) + adapter := adapters.NewEC2TransitGatewayRouteTablePropagationAdapter(testClient, testAWSConfig.AccountID, testAWSConfig.Region, sdpcache.NewNoOpCache()) + + if err := adapter.Validate(); err != nil { + t.Fatalf("failed to validate adapter: %v", err) + } + + items, err := adapter.List(ctx, scope, true) + if err != nil { + t.Fatalf("failed to list transit gateway route table propagations: %v", err) + } + + if len(items) == 0 { + t.Fatalf("expected at least one propagation (Setup creates a TGW VPC attachment); got 0") + } + + query := items[0].UniqueAttributeValue() + got, err := adapter.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get propagation %s: %v", query, err) + } + if got.UniqueAttributeValue() != query { + t.Fatalf("expected %s, got %s", query, got.UniqueAttributeValue()) + } + + // Search by route table ID (used by route table → propagation link). + if createdRouteTableID != "" { + searchItems, err := adapter.Search(ctx, scope, createdRouteTableID, true) + if err != nil { + t.Fatalf("failed to search propagations by route table ID %s: %v", createdRouteTableID, err) + } + if len(searchItems) == 0 { + t.Fatalf("expected at least one propagation for route table %s (Setup creates one); got 0", createdRouteTableID) + } + } +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_test.go b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_test.go new file mode 100644 index 00000000..1bbb6ba3 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_test.go @@ -0,0 +1,111 @@ +package ec2transitgateway + +import ( + "context" + "fmt" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters" + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/sdpcache" +) + +// TransitGatewayRouteTable runs the integration test for the transit gateway route table adapter. +// +// AWS CLI – list route tables (same data this test lists/gets/searches): +// +// aws ec2 describe-transit-gateway-route-tables [--region REGION] +// +// AWS Console – Transit Gateway route tables: +// +// https://[REGION].console.aws.amazon.com/ec2/home?region=[REGION]#TransitGatewayRouteTables: +// +// Overmind – In the app, open your AWS source and search for type ec2-transit-gateway-route-table +// or navigate to the resource type in the source. +func TransitGatewayRouteTable(t *testing.T) { + ctx := context.Background() + + testClient, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + t.Fatalf("Failed to get AWS settings: %v", err) + } + + accountID := testAWSConfig.AccountID + scope := adapters.FormatScope(accountID, testAWSConfig.Region) + + adapter := adapters.NewEC2TransitGatewayRouteTableAdapter(testClient, accountID, testAWSConfig.Region, sdpcache.NewNoOpCache()) + + if err := adapter.Validate(); err != nil { + t.Fatalf("failed to validate transit gateway route table adapter: %v", err) + } + + items, err := listSync(adapter, ctx, scope, true) + if err != nil { + t.Fatalf("failed to list transit gateway route tables: %v", err) + } + + tgwID, err := getIntegrationTestTransitGatewayID(ctx, testClient) + if err != nil { + t.Fatalf("failed to get integration-test transit gateway ID: %v", err) + } + + // Find the route table for the transit gateway created in Setup (or discovered by tag). + var routeTableID string + for _, item := range items { + tgwIDVal, _ := item.GetAttributes().Get("TransitGatewayId") + if tgwIDVal != nil { + if id, ok := tgwIDVal.(string); ok && id == tgwID { + routeTableID = item.UniqueAttributeValue() + break + } + } + } + if routeTableID == "" { + t.Fatalf("no route table found for transit gateway %s (created in Setup)", tgwID) + } + + got, err := adapter.Get(ctx, scope, routeTableID, true) + if err != nil { + t.Fatalf("failed to get transit gateway route table %s: %v", routeTableID, err) + } + + if got.UniqueAttributeValue() != routeTableID { + t.Fatalf("expected route table ID %s from Get, got %s", routeTableID, got.UniqueAttributeValue()) + } + + arn := fmt.Sprintf("arn:aws:ec2:%s:%s:transit-gateway-route-table/%s", testAWSConfig.Region, accountID, routeTableID) + searchItems, err := searchSync(adapter, ctx, scope, arn, true) + if err != nil { + t.Fatalf("failed to search transit gateway route table by ARN: %v", err) + } + + if len(searchItems) == 0 { + t.Fatalf("search by ARN returned no items") + } + + if searchItems[0].UniqueAttributeValue() != routeTableID { + t.Fatalf("expected route table ID %s from Search, got %s", routeTableID, searchItems[0].UniqueAttributeValue()) + } + + // Route table links to associations, propagations, and routes (Search by route table ID). + links := got.GetLinkedItemQueries() + if len(links) < 4 { + t.Fatalf("expected at least 4 linked item queries (ec2-transit-gateway + 3 Search links); got %d", len(links)) + } + linkTypes := make(map[string]bool) + for _, l := range links { + if l.GetQuery() != nil { + linkTypes[l.GetQuery().GetType()] = true + } + } + for _, want := range []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table-association", "ec2-transit-gateway-route-table-propagation", "ec2-transit-gateway-route"} { + if !linkTypes[want] { + t.Errorf("expected route table to link to %s", want) + } + } +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_test.go b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_test.go new file mode 100644 index 00000000..1458a88b --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_test.go @@ -0,0 +1,62 @@ +package ec2transitgateway + +import ( + "context" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters" + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/sdpcache" +) + +// TransitGatewayRoute runs the integration test for the transit gateway route adapter. +// Setup creates a static route in the default route table, so we get at least one route. +func TransitGatewayRoute(t *testing.T) { + ctx := context.Background() + + testClient, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + t.Fatalf("Failed to get AWS settings: %v", err) + } + + scope := adapters.FormatScope(testAWSConfig.AccountID, testAWSConfig.Region) + adapter := adapters.NewEC2TransitGatewayRouteAdapter(testClient, testAWSConfig.AccountID, testAWSConfig.Region, sdpcache.NewNoOpCache()) + + if err := adapter.Validate(); err != nil { + t.Fatalf("failed to validate adapter: %v", err) + } + + items, err := adapter.List(ctx, scope, true) + if err != nil { + t.Fatalf("failed to list transit gateway routes: %v", err) + } + + if len(items) == 0 { + t.Fatalf("expected at least one route (Setup creates a static TGW route); got 0") + } + + query := items[0].UniqueAttributeValue() + got, err := adapter.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get route %s: %v", query, err) + } + if got.UniqueAttributeValue() != query { + t.Fatalf("expected %s, got %s", query, got.UniqueAttributeValue()) + } + + // Search by route table ID (used by route table → route link). + if createdRouteTableID != "" { + searchItems, err := adapter.Search(ctx, scope, createdRouteTableID, true) + if err != nil { + t.Fatalf("failed to search routes by route table ID %s: %v", createdRouteTableID, err) + } + if len(searchItems) == 0 { + t.Fatalf("expected at least one route for route table %s (Setup creates a static route); got 0", createdRouteTableID) + } + } +} diff --git a/aws-source/adapters/integration/ec2/instance_test.go b/aws-source/adapters/integration/ec2/instance_test.go index 730339ef..56b1d241 100644 --- a/aws-source/adapters/integration/ec2/instance_test.go +++ b/aws-source/adapters/integration/ec2/instance_test.go @@ -7,9 +7,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/kms/kms_test.go b/aws-source/adapters/integration/kms/kms_test.go index 3733d15c..750a7730 100644 --- a/aws-source/adapters/integration/kms/kms_test.go +++ b/aws-source/adapters/integration/kms/kms_test.go @@ -8,9 +8,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/networkmanager/networkmanager_test.go b/aws-source/adapters/integration/networkmanager/networkmanager_test.go index 7a2f490f..712f1ad2 100644 --- a/aws-source/adapters/integration/networkmanager/networkmanager_test.go +++ b/aws-source/adapters/integration/networkmanager/networkmanager_test.go @@ -8,9 +8,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/ssm/main_test.go b/aws-source/adapters/integration/ssm/main_test.go index db6b64ab..ab13915c 100644 --- a/aws-source/adapters/integration/ssm/main_test.go +++ b/aws-source/adapters/integration/ssm/main_test.go @@ -14,9 +14,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ssm/types" "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/tracing" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) diff --git a/aws-source/adapters/integration/util.go b/aws-source/adapters/integration/util.go index e6828249..1b2b1b4a 100644 --- a/aws-source/adapters/integration/util.go +++ b/aws-source/adapters/integration/util.go @@ -13,8 +13,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" ) const ( diff --git a/aws-source/adapters/kms-alias.go b/aws-source/adapters/kms-alias.go index 0272a291..ce0c8fd2 100644 --- a/aws-source/adapters/kms-alias.go +++ b/aws-source/adapters/kms-alias.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func aliasOutputMapper(_ context.Context, _ *kms.Client, scope string, _ *kms.ListAliasesInput, output *kms.ListAliasesOutput) ([]*sdp.Item, error) { @@ -64,12 +64,6 @@ func aliasOutputMapper(_ context.Context, _ *kms.Client, scope string, _ *kms.Li Query: *alias.TargetKeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - // Adding, deleting, or updating an alias can allow or deny permission to the KMS key. - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/kms-alias_test.go b/aws-source/adapters/kms-alias_test.go index 4b50aa6c..3e99443a 100644 --- a/aws-source/adapters/kms-alias_test.go +++ b/aws-source/adapters/kms-alias_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAliasOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/kms-custom-key-store.go b/aws-source/adapters/kms-custom-key-store.go index ddaa0742..b9c759fd 100644 --- a/aws-source/adapters/kms-custom-key-store.go +++ b/aws-source/adapters/kms-custom-key-store.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func customKeyStoreOutputMapper(_ context.Context, _ *kms.Client, scope string, _ *kms.DescribeCustomKeyStoresInput, output *kms.DescribeCustomKeyStoresOutput) ([]*sdp.Item, error) { @@ -54,12 +54,6 @@ func customKeyStoreOutputMapper(_ context.Context, _ *kms.Client, scope string, Query: *customKeyStore.CloudHsmClusterId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the CloudHSM cluster will affect the custom key store - In: true, - // Updating the custom key store will not affect the CloudHSM cluster - Out: false, - }, }) } @@ -72,12 +66,6 @@ func customKeyStoreOutputMapper(_ context.Context, _ *kms.Client, scope string, Query: fmt.Sprintf("name|%s", *customKeyStore.XksProxyConfiguration.VpcEndpointServiceName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC endpoint service will affect the custom key store - In: true, - // Updating the custom key store will not affect the VPC endpoint service - Out: false, - }, }) } diff --git a/aws-source/adapters/kms-custom-key-store_test.go b/aws-source/adapters/kms-custom-key-store_test.go index 10a420b0..19c2d834 100644 --- a/aws-source/adapters/kms-custom-key-store_test.go +++ b/aws-source/adapters/kms-custom-key-store_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestCustomKeyStoreOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/kms-grant.go b/aws-source/adapters/kms-grant.go index 66b8bd49..2d3e90cc 100644 --- a/aws-source/adapters/kms-grant.go +++ b/aws-source/adapters/kms-grant.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" ) @@ -63,12 +63,6 @@ func grantOutputMapper(ctx context.Context, _ *kms.Client, scope string, _ *kms. Query: keyID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - // Adding or revoking/retiring a grant can allow or deny permission to the KMS key for the grantee. - In: true, - Out: true, - }, }) var principals []string @@ -123,13 +117,6 @@ func grantOutputMapper(ctx context.Context, _ *kms.Client, scope string, _ *kms. Method: sdp.QueryMethod_GET, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - // Adding or revoking/retiring a grant can allow or deny permission to the KMS key for the grantee. - // Or, disabling a role will make the grant redundant. - In: true, - Out: true, - }, } arn, errA := ParseARN(principal) diff --git a/aws-source/adapters/kms-grant_test.go b/aws-source/adapters/kms-grant_test.go index 3dd5a08a..c2f8dbf9 100644 --- a/aws-source/adapters/kms-grant_test.go +++ b/aws-source/adapters/kms-grant_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/kms-key-policy.go b/aws-source/adapters/kms-key-policy.go index aa2895c7..6ed8eef4 100644 --- a/aws-source/adapters/kms-key-policy.go +++ b/aws-source/adapters/kms-key-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" ) @@ -75,11 +75,6 @@ func getKeyPolicyFunc(ctx context.Context, client keyPolicyClient, scope string, Query: *input.KeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - }, }) return item, nil diff --git a/aws-source/adapters/kms-key-policy_test.go b/aws-source/adapters/kms-key-policy_test.go index 96682d43..be46f433 100644 --- a/aws-source/adapters/kms-key-policy_test.go +++ b/aws-source/adapters/kms-key-policy_test.go @@ -6,8 +6,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/kms-key.go b/aws-source/adapters/kms-key.go index cf7db93b..ca9bc3e9 100644 --- a/aws-source/adapters/kms-key.go +++ b/aws-source/adapters/kms-key.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type kmsClient interface { @@ -62,12 +62,6 @@ func kmsKeyGetFunc(ctx context.Context, client kmsClient, scope string, input *k Query: *output.KeyMetadata.CustomKeyStoreId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // A keystore cannot be deleted if it contains a key. - In: true, - // Any change on the key won't affect the keystore. - Out: false, - }, }) } @@ -78,11 +72,6 @@ func kmsKeyGetFunc(ctx context.Context, client kmsClient, scope string, input *k Query: *input.KeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -92,11 +81,6 @@ func kmsKeyGetFunc(ctx context.Context, client kmsClient, scope string, input *k Query: *input.KeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) switch output.KeyMetadata.KeyState { diff --git a/aws-source/adapters/kms-key_test.go b/aws-source/adapters/kms-key_test.go index 5d88422c..9927110e 100644 --- a/aws-source/adapters/kms-key_test.go +++ b/aws-source/adapters/kms-key_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type kmsTestClient struct{} diff --git a/aws-source/adapters/lambda-event-source-mapping.go b/aws-source/adapters/lambda-event-source-mapping.go index 5a53be8a..8a3f134f 100644 --- a/aws-source/adapters/lambda-event-source-mapping.go +++ b/aws-source/adapters/lambda-event-source-mapping.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type lambdaEventSourceMappingClient interface { @@ -99,11 +99,6 @@ func eventSourceMappingOutputMapper(query, scope string, awsItem *types.EventSou Query: *awsItem.FunctionArn, Scope: FormatScope(parsedARN.AccountID, parsedARN.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } } @@ -143,12 +138,6 @@ func eventSourceMappingOutputMapper(query, scope string, awsItem *types.EventSou Query: *awsItem.EventSourceArn, Scope: FormatScope(parsedARN.AccountID, parsedARN.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the event source will affect the mapping - In: true, - // Changing the mapping won't affect the event source - Out: false, - }, }) } } diff --git a/aws-source/adapters/lambda-event-source-mapping_test.go b/aws-source/adapters/lambda-event-source-mapping_test.go index e671c50d..9a9206ad 100644 --- a/aws-source/adapters/lambda-event-source-mapping_test.go +++ b/aws-source/adapters/lambda-event-source-mapping_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type TestLambdaEventSourceMappingClient struct{} diff --git a/aws-source/adapters/lambda-function.go b/aws-source/adapters/lambda-function.go index 5295ed26..8f363eeb 100644 --- a/aws-source/adapters/lambda-function.go +++ b/aws-source/adapters/lambda-function.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type FunctionDetails struct { @@ -148,11 +148,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: u.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: false, - }, }) } } @@ -165,12 +160,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Code.ImageUri, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the image will affect the function - In: true, - // Changing the function won't affect the image - Out: false, - }, }) } @@ -182,12 +171,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Code.ResolvedImageUri, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the image will affect the function - In: true, - // Changing the function won't affect the image - Out: false, - }, }) } } @@ -223,12 +206,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.Role, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the role will affect the function - In: true, - // Changing the function won't affect the role - Out: false, - }, }) } } @@ -259,11 +236,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *fsConfig.Arn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are really tightly linked - In: true, - Out: true, - }, }) } } @@ -278,12 +250,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.KMSKeyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key will affect the function - In: true, - // Changing the function won't affect the key - Out: false, - }, }) } } @@ -301,11 +267,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: name, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -319,12 +280,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *layer.SigningJobArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the signing will affect the function - In: true, - // Changing the function won't affect the signing - Out: false, - }, }) } } @@ -338,12 +293,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *layer.SigningProfileVersionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the signing will affect the function - In: true, - // Changing the function won't affect the signing - Out: false, - }, }) } } @@ -358,11 +307,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.MasterArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } @@ -376,12 +320,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.SigningJobArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the signing will affect the function - In: true, - // Changing the function won't affect the signing - Out: false, - }, }) } } @@ -395,12 +333,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.SigningProfileVersionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the signing will affect the function - In: true, - // Changing the function won't affect the signing - Out: false, - }, }) } } @@ -414,12 +346,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the function - In: true, - // Changing the function won't affect the security group - Out: false, - }, }) } @@ -431,12 +357,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the function - In: true, - // Changing the function won't affect the subnet - Out: false, - }, }) } @@ -448,7 +368,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.VpcConfig.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{}, }) } } @@ -463,11 +382,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *config.FunctionUrl, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -550,12 +464,6 @@ func ExtractLinksFromPolicy(policy *PolicyDocument) []*sdp.LinkedItemQuery { Query: statement.Condition.ArnLike.AWSSourceArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing a lambda shouldn't affect the upstream source - Out: false, - // Changing the source should affect the lambda - In: true, - }, }) } @@ -582,11 +490,6 @@ func GetEventLinkedItem(destinationARN string) (*sdp.LinkedItemQuery, error) { Query: destinationARN, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, nil case "sqs": return &sdp.LinkedItemQuery{ @@ -596,11 +499,6 @@ func GetEventLinkedItem(destinationARN string) (*sdp.LinkedItemQuery, error) { Query: destinationARN, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, nil case "lambda": return &sdp.LinkedItemQuery{ @@ -610,11 +508,6 @@ func GetEventLinkedItem(destinationARN string) (*sdp.LinkedItemQuery, error) { Query: destinationARN, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, nil case "events": return &sdp.LinkedItemQuery{ @@ -624,11 +517,6 @@ func GetEventLinkedItem(destinationARN string) (*sdp.LinkedItemQuery, error) { Query: destinationARN, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, nil } diff --git a/aws-source/adapters/lambda-function_test.go b/aws-source/adapters/lambda-function_test.go index f6613dfe..8a444c81 100644 --- a/aws-source/adapters/lambda-function_test.go +++ b/aws-source/adapters/lambda-function_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var testFuncConfig = &types.FunctionConfiguration{ diff --git a/aws-source/adapters/lambda-layer-version.go b/aws-source/adapters/lambda-layer-version.go index 522cfd52..a62e6d71 100644 --- a/aws-source/adapters/lambda-layer-version.go +++ b/aws-source/adapters/lambda-layer-version.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func layerVersionGetInputMapper(scope, query string) *lambda.GetLayerVersionInput { @@ -75,12 +75,6 @@ func layerVersionGetFunc(ctx context.Context, client LambdaClient, scope string, Query: *out.Content.SigningJobArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Signing jobs can affect layers - In: true, - // Changing the layer won't affect the signing job - Out: false, - }, }) } } @@ -94,12 +88,6 @@ func layerVersionGetFunc(ctx context.Context, client LambdaClient, scope string, Query: *out.Content.SigningProfileVersionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Signing profiles can affect layers - In: true, - // Changing the layer won't affect the signing profile - Out: false, - }, }) } } diff --git a/aws-source/adapters/lambda-layer-version_test.go b/aws-source/adapters/lambda-layer-version_test.go index 09c06411..15d1f253 100644 --- a/aws-source/adapters/lambda-layer-version_test.go +++ b/aws-source/adapters/lambda-layer-version_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLayerVersionGetInputMapper(t *testing.T) { diff --git a/aws-source/adapters/lambda-layer.go b/aws-source/adapters/lambda-layer.go index e467bf55..2b68992d 100644 --- a/aws-source/adapters/lambda-layer.go +++ b/aws-source/adapters/lambda-layer.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func layerListFunc(ctx context.Context, client *lambda.Client, scope string) ([]*types.LayersListItem, error) { @@ -53,11 +53,6 @@ func layerItemMapper(_, scope string, awsItem *types.LayersListItem) (*sdp.Item, Query: fmt.Sprintf("%v:%v", *awsItem.LayerName, awsItem.LatestMatchingVersion.Version), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/lambda-layer_test.go b/aws-source/adapters/lambda-layer_test.go index 71713317..d402bdd1 100644 --- a/aws-source/adapters/lambda-layer_test.go +++ b/aws-source/adapters/lambda-layer_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLayerItemMapper(t *testing.T) { diff --git a/aws-source/adapters/main.go b/aws-source/adapters/main.go index 6a5baa27..856e1a97 100644 --- a/aws-source/adapters/main.go +++ b/aws-source/adapters/main.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) var Metadata = sdp.AdapterMetadataList{} diff --git a/aws-source/adapters/network-firewall-firewall-policy.go b/aws-source/adapters/network-firewall-firewall-policy.go index daf15cb1..a2621015 100644 --- a/aws-source/adapters/network-firewall-firewall-policy.go +++ b/aws-source/adapters/network-firewall-firewall-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type unifiedFirewallPolicy struct { @@ -88,10 +88,6 @@ func firewallPolicyGetFunc(ctx context.Context, client networkFirewallClient, sc Method: sdp.QueryMethod_SEARCH, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -106,10 +102,6 @@ func firewallPolicyGetFunc(ctx context.Context, client networkFirewallClient, sc Query: *resp.FirewallPolicy.TLSInspectionConfigurationArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/network-firewall-firewall-policy_test.go b/aws-source/adapters/network-firewall-firewall-policy_test.go index 109185c5..092aab28 100644 --- a/aws-source/adapters/network-firewall-firewall-policy_test.go +++ b/aws-source/adapters/network-firewall-firewall-policy_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/network-firewall-firewall.go b/aws-source/adapters/network-firewall-firewall.go index f67e47a3..ad366de3 100644 --- a/aws-source/adapters/network-firewall-firewall.go +++ b/aws-source/adapters/network-firewall-firewall.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type unifiedFirewall struct { @@ -118,10 +118,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: logGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } case types.LogDestinationTypeS3: @@ -136,10 +132,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: bucketName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } case types.LogDestinationTypeKinesisDataFirehose: @@ -154,10 +146,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: deliveryStream, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } @@ -173,10 +161,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *uf.ResourcePolicy, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -190,11 +174,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *config.FirewallPolicyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Policy will affect the firewall but not the other way around - In: true, - Out: false, - }, }) } } @@ -209,11 +188,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *mapping.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to public subnets could affect the firewall - In: true, - Out: false, - }, }) } } @@ -227,11 +201,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *config.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the VPC could affect the firewall - In: true, - Out: false, - }, }) } @@ -248,11 +217,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *state.Attachment.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to public subnets could affect the firewall - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/network-firewall-firewall_test.go b/aws-source/adapters/network-firewall-firewall_test.go index 4417e200..1092e319 100644 --- a/aws-source/adapters/network-firewall-firewall_test.go +++ b/aws-source/adapters/network-firewall-firewall_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func (c testNetworkFirewallClient) DescribeFirewall(ctx context.Context, params *networkfirewall.DescribeFirewallInput, optFns ...func(*networkfirewall.Options)) (*networkfirewall.DescribeFirewallOutput, error) { diff --git a/aws-source/adapters/network-firewall-rule-group.go b/aws-source/adapters/network-firewall-rule-group.go index 1af444c2..a8c5fdca 100644 --- a/aws-source/adapters/network-firewall-rule-group.go +++ b/aws-source/adapters/network-firewall-rule-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type unifiedRuleGroup struct { @@ -79,10 +79,6 @@ func ruleGroupGetFunc(ctx context.Context, client networkFirewallClient, scope s Query: *resp.RuleGroupResponse.SnsTopic, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } @@ -97,10 +93,6 @@ func ruleGroupGetFunc(ctx context.Context, client networkFirewallClient, scope s Query: *resp.RuleGroupResponse.SourceMetadata.SourceArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: false, - }, }) } } diff --git a/aws-source/adapters/network-firewall-rule-group_test.go b/aws-source/adapters/network-firewall-rule-group_test.go index d4773adc..c5dd65a6 100644 --- a/aws-source/adapters/network-firewall-rule-group_test.go +++ b/aws-source/adapters/network-firewall-rule-group_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/network-firewall-tls-inspection-configuration.go b/aws-source/adapters/network-firewall-tls-inspection-configuration.go index d2354ab8..90b16bfd 100644 --- a/aws-source/adapters/network-firewall-tls-inspection-configuration.go +++ b/aws-source/adapters/network-firewall-tls-inspection-configuration.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type unifiedTLSInspectionConfiguration struct { @@ -82,10 +82,6 @@ func tlsInspectionConfigurationGetFunc(ctx context.Context, client networkFirewa Query: *utic.Properties.CertificateAuthority.CertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -102,10 +98,6 @@ func tlsInspectionConfigurationGetFunc(ctx context.Context, client networkFirewa Query: *cert.CertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -124,10 +116,6 @@ func tlsInspectionConfigurationGetFunc(ctx context.Context, client networkFirewa Query: *config.CertificateAuthorityArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -143,10 +131,6 @@ func tlsInspectionConfigurationGetFunc(ctx context.Context, client networkFirewa Query: *serverCert.ResourceArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go b/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go index bd623260..1ff37cdb 100644 --- a/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go +++ b/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/networkfirewall.go b/aws-source/adapters/networkfirewall.go index 45a2bb8a..18ead662 100644 --- a/aws-source/adapters/networkfirewall.go +++ b/aws-source/adapters/networkfirewall.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type networkFirewallClient interface { @@ -36,10 +36,6 @@ func encryptionConfigurationLink(config *types.EncryptionConfiguration, scope st Query: *config.KeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } } else { return &sdp.LinkedItemQuery{ @@ -49,10 +45,6 @@ func encryptionConfigurationLink(config *types.EncryptionConfiguration, scope st Query: *config.KeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } } } diff --git a/aws-source/adapters/networkmanager-connect-attachment.go b/aws-source/adapters/networkmanager-connect-attachment.go index df3d4010..5c2afb45 100644 --- a/aws-source/adapters/networkmanager-connect-attachment.go +++ b/aws-source/adapters/networkmanager-connect-attachment.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func connectAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.ConnectAttachment, error) { @@ -50,10 +50,6 @@ func connectAttachmentItemMapper(_, scope string, ca *types.ConnectAttachment) ( Query: *ca.Attachment.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -66,10 +62,6 @@ func connectAttachmentItemMapper(_, scope string, ca *types.ConnectAttachment) ( Query: *ca.Attachment.CoreNetworkArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkmanager-connect-attachment_test.go b/aws-source/adapters/networkmanager-connect-attachment_test.go index d8e398e5..7e9479fe 100644 --- a/aws-source/adapters/networkmanager-connect-attachment_test.go +++ b/aws-source/adapters/networkmanager-connect-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestConnectAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-connect-peer-association.go b/aws-source/adapters/networkmanager-connect-peer-association.go index 0624ac89..115c0b93 100644 --- a/aws-source/adapters/networkmanager-connect-peer-association.go +++ b/aws-source/adapters/networkmanager-connect-peer-association.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetConnectPeerAssociationsInput, output *networkmanager.GetConnectPeerAssociationsOutput) ([]*sdp.Item, error) { @@ -46,10 +46,6 @@ func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Cl Query: *a.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -58,10 +54,6 @@ func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Cl Query: *a.ConnectPeerId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -85,10 +77,6 @@ func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Cl Query: idWithGlobalNetwork(*a.GlobalNetworkId, *a.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -100,10 +88,6 @@ func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Cl Query: idWithGlobalNetwork(*a.GlobalNetworkId, *a.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } diff --git a/aws-source/adapters/networkmanager-connect-peer-association_test.go b/aws-source/adapters/networkmanager-connect-peer-association_test.go index 8d2e1188..501a4c56 100644 --- a/aws-source/adapters/networkmanager-connect-peer-association_test.go +++ b/aws-source/adapters/networkmanager-connect-peer-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestConnectPeerAssociationsOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-connect-peer.go b/aws-source/adapters/networkmanager-connect-peer.go index 99742bc7..5e71b859 100644 --- a/aws-source/adapters/networkmanager-connect-peer.go +++ b/aws-source/adapters/networkmanager-connect-peer.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope string, input *networkmanager.GetConnectPeerInput) (*sdp.Item, error) { @@ -43,10 +43,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.Configuration.CoreNetworkAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -59,10 +55,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.Configuration.PeerAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -76,10 +68,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *config.CoreNetworkAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) if config.PeerAddress != nil { @@ -91,10 +79,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *config.PeerAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -107,10 +91,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: strconv.FormatInt(*config.CoreNetworkAsn, 10), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -123,10 +103,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: strconv.FormatInt(*config.PeerAsn, 10), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -141,10 +117,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -158,10 +130,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.SubnetArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -175,10 +143,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.ConnectAttachmentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-connect-peer_test.go b/aws-source/adapters/networkmanager-connect-peer_test.go index 0cf3c2e3..c653214d 100644 --- a/aws-source/adapters/networkmanager-connect-peer_test.go +++ b/aws-source/adapters/networkmanager-connect-peer_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func (n NetworkManagerTestClient) GetConnectPeer(ctx context.Context, params *networkmanager.GetConnectPeerInput, optFns ...func(*networkmanager.Options)) (*networkmanager.GetConnectPeerOutput, error) { diff --git a/aws-source/adapters/networkmanager-connection.go b/aws-source/adapters/networkmanager-connection.go index 8c3c476b..a439d146 100644 --- a/aws-source/adapters/networkmanager-connection.go +++ b/aws-source/adapters/networkmanager-connection.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetConnectionsInput, output *networkmanager.GetConnectionsOutput) ([]*sdp.Item, error) { @@ -47,10 +47,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -63,10 +59,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -78,10 +70,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.ConnectedLinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -93,10 +81,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -108,10 +92,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.ConnectedDeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-connection_test.go b/aws-source/adapters/networkmanager-connection_test.go index c873431c..a2cf5f1d 100644 --- a/aws-source/adapters/networkmanager-connection_test.go +++ b/aws-source/adapters/networkmanager-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-core-network-policy.go b/aws-source/adapters/networkmanager-core-network-policy.go index fd934323..c46ce479 100644 --- a/aws-source/adapters/networkmanager-core-network-policy.go +++ b/aws-source/adapters/networkmanager-core-network-policy.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func coreNetworkPolicyGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.CoreNetworkPolicy, error) { @@ -45,10 +45,6 @@ func coreNetworkPolicyItemMapper(_, scope string, cn *types.CoreNetworkPolicy) ( Query: *cn.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } diff --git a/aws-source/adapters/networkmanager-core-network-policy_test.go b/aws-source/adapters/networkmanager-core-network-policy_test.go index 5cbbb949..b0bd39c5 100644 --- a/aws-source/adapters/networkmanager-core-network-policy_test.go +++ b/aws-source/adapters/networkmanager-core-network-policy_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestCoreNetworkPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-core-network.go b/aws-source/adapters/networkmanager-core-network.go index bd393cb8..8e9d8da1 100644 --- a/aws-source/adapters/networkmanager-core-network.go +++ b/aws-source/adapters/networkmanager-core-network.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope string, input *networkmanager.GetCoreNetworkInput) (*sdp.Item, error) { @@ -44,10 +44,6 @@ func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -56,10 +52,6 @@ func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, }, } @@ -72,10 +64,6 @@ func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -88,10 +76,6 @@ func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: strconv.FormatInt(*edge.Asn, 10), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkmanager-core-network_test.go b/aws-source/adapters/networkmanager-core-network_test.go index e90a7faa..9283b49d 100644 --- a/aws-source/adapters/networkmanager-core-network_test.go +++ b/aws-source/adapters/networkmanager-core-network_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func (n NetworkManagerTestClient) GetCoreNetwork(ctx context.Context, params *networkmanager.GetCoreNetworkInput, optFns ...func(*networkmanager.Options)) (*networkmanager.GetCoreNetworkOutput, error) { diff --git a/aws-source/adapters/networkmanager-device.go b/aws-source/adapters/networkmanager-device.go index cfbd8686..6201423d 100644 --- a/aws-source/adapters/networkmanager-device.go +++ b/aws-source/adapters/networkmanager-device.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetDevicesInput, output *networkmanager.GetDevicesOutput) ([]*sdp.Item, error) { @@ -47,10 +47,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -59,10 +55,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: idWithTypeAndGlobalNetwork(*s.GlobalNetworkId, "device", *s.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -71,10 +63,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -87,10 +75,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.SiteId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -102,10 +86,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.DeviceArn), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-device_test.go b/aws-source/adapters/networkmanager-device_test.go index 76f432e1..190278c3 100644 --- a/aws-source/adapters/networkmanager-device_test.go +++ b/aws-source/adapters/networkmanager-device_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDeviceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-global-network.go b/aws-source/adapters/networkmanager-global-network.go index 7918d5b1..9c120ef5 100644 --- a/aws-source/adapters/networkmanager-global-network.go +++ b/aws-source/adapters/networkmanager-global-network.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, scope string, _ *networkmanager.DescribeGlobalNetworksInput, output *networkmanager.DescribeGlobalNetworksOutput) ([]*sdp.Item, error) { @@ -41,10 +41,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -53,10 +49,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -65,10 +57,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -77,10 +65,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -89,10 +73,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -101,10 +81,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -113,10 +89,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -125,10 +97,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -137,10 +105,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, }, } diff --git a/aws-source/adapters/networkmanager-global-network_test.go b/aws-source/adapters/networkmanager-global-network_test.go index 2dfd5299..8407c7b7 100644 --- a/aws-source/adapters/networkmanager-global-network_test.go +++ b/aws-source/adapters/networkmanager-global-network_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestGlobalNetworkOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-link-association.go b/aws-source/adapters/networkmanager-link-association.go index 971d705a..a4fc454f 100644 --- a/aws-source/adapters/networkmanager-link-association.go +++ b/aws-source/adapters/networkmanager-link-association.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetLinkAssociationsInput, output *networkmanager.GetLinkAssociationsOutput) ([]*sdp.Item, error) { @@ -48,10 +48,6 @@ func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, sc Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -60,10 +56,6 @@ func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, sc Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -72,10 +64,6 @@ func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, sc Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } diff --git a/aws-source/adapters/networkmanager-link-association_test.go b/aws-source/adapters/networkmanager-link-association_test.go index f753e9ea..cb74c060 100644 --- a/aws-source/adapters/networkmanager-link-association_test.go +++ b/aws-source/adapters/networkmanager-link-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestLinkAssociationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-link.go b/aws-source/adapters/networkmanager-link.go index 36c51466..9858d8bd 100644 --- a/aws-source/adapters/networkmanager-link.go +++ b/aws-source/adapters/networkmanager-link.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetLinksInput, output *networkmanager.GetLinksOutput) ([]*sdp.Item, error) { @@ -43,10 +43,6 @@ func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -55,10 +51,6 @@ func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithTypeAndGlobalNetwork(*s.GlobalNetworkId, "link", *s.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } @@ -71,10 +63,6 @@ func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.SiteId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -86,10 +74,6 @@ func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.LinkArn), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-link_test.go b/aws-source/adapters/networkmanager-link_test.go index 4f3c3e60..363d6838 100644 --- a/aws-source/adapters/networkmanager-link_test.go +++ b/aws-source/adapters/networkmanager-link_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLinkOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-network-resource-relationship.go b/aws-source/adapters/networkmanager-network-resource-relationship.go index df7961dd..cd6019cb 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, input *networkmanager.GetNetworkResourceRelationshipsInput, output *networkmanager.GetNetworkResourceRelationshipsOutput) ([]*sdp.Item, error) { @@ -71,10 +71,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: idWithGlobalNetwork(*input.GlobalNetworkId, toArn.ResourceID()), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "networkmanager-device": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -84,10 +80,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: idWithGlobalNetwork(*input.GlobalNetworkId, toArn.ResourceID()), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "networkmanager-link": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -97,10 +89,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: idWithGlobalNetwork(*input.GlobalNetworkId, toArn.ResourceID()), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "networkmanager-site": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -110,10 +98,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: idWithGlobalNetwork(*input.GlobalNetworkId, toArn.ResourceID()), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "directconnect-connection": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -123,10 +107,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "directconnect-direct-connect-gateway": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -136,10 +116,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "directconnect-virtual-interface": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -149,10 +125,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-customer-gateway": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -162,10 +134,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-transit-gateway": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -175,10 +143,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-transit-gateway-attachment": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -188,10 +152,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-transit-gateway-connect-peer": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -201,10 +161,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-transit-gateway-route-table": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -214,10 +170,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-vpn-connection": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -227,10 +179,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) default: // skip unknown item types diff --git a/aws-source/adapters/networkmanager-network-resource-relationship_test.go b/aws-source/adapters/networkmanager-network-resource-relationship_test.go index 6a18cfbd..78dd910c 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship_test.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestNetworkResourceRelationshipOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go index 30ad792c..a0a6af96 100644 --- a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go +++ b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func getSiteToSiteVpnAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.SiteToSiteVpnAttachment, error) { @@ -49,10 +49,6 @@ func siteToSiteVpnAttachmentItemMapper(_, scope string, awsItem *types.SiteToSit Query: *awsItem.Attachment.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -77,10 +73,6 @@ func siteToSiteVpnAttachmentItemMapper(_, scope string, awsItem *types.SiteToSit Query: *awsItem.VpnConnectionArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go index 19e47712..473d8e11 100644 --- a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go +++ b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestSiteToSiteVpnAttachmentOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-site.go b/aws-source/adapters/networkmanager-site.go index e8024478..fa87210b 100644 --- a/aws-source/adapters/networkmanager-site.go +++ b/aws-source/adapters/networkmanager-site.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetSitesInput, output *networkmanager.GetSitesOutput) ([]*sdp.Item, error) { @@ -47,10 +47,6 @@ func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -59,10 +55,6 @@ func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.SiteId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -71,10 +63,6 @@ func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.SiteId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } diff --git a/aws-source/adapters/networkmanager-site_test.go b/aws-source/adapters/networkmanager-site_test.go index 94299f3f..9c2e885f 100644 --- a/aws-source/adapters/networkmanager-site_test.go +++ b/aws-source/adapters/networkmanager-site_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSiteOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go index 7b003117..1211ceae 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go +++ b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetTransitGatewayConnectPeerAssociationsInput, output *networkmanager.GetTransitGatewayConnectPeerAssociationsOutput) ([]*sdp.Item, error) { @@ -41,10 +41,6 @@ func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *net Query: *a.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -57,10 +53,6 @@ func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *net Query: idWithGlobalNetwork(*a.GlobalNetworkId, *a.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -72,10 +64,6 @@ func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *net Query: idWithGlobalNetwork(*a.GlobalNetworkId, *a.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go index f0afd62f..6db8a6c8 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTransitGatewayConnectPeerAssociationsOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-peering.go b/aws-source/adapters/networkmanager-transit-gateway-peering.go index 7e840206..0f2ecdca 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-peering.go +++ b/aws-source/adapters/networkmanager-transit-gateway-peering.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func getTransitGatewayPeeringGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.TransitGatewayPeering, error) { @@ -49,10 +49,6 @@ func transitGatewayPeeringItemMapper(_, scope string, awsItem *types.TransitGate Query: *awsItem.Peering.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -75,10 +71,6 @@ func transitGatewayPeeringItemMapper(_, scope string, awsItem *types.TransitGate Query: *awsItem.TransitGatewayPeeringAttachmentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -92,10 +84,6 @@ func transitGatewayPeeringItemMapper(_, scope string, awsItem *types.TransitGate Query: *awsItem.TransitGatewayArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/networkmanager-transit-gateway-peering_test.go b/aws-source/adapters/networkmanager-transit-gateway-peering_test.go index 3b530c94..03c8fd28 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-peering_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-peering_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTransitGatewayPeeringOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-registration.go b/aws-source/adapters/networkmanager-transit-gateway-registration.go index d0d1c3a0..f695c818 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-registration.go +++ b/aws-source/adapters/networkmanager-transit-gateway-registration.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func transitGatewayRegistrationOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetTransitGatewayRegistrationsInput, output *networkmanager.GetTransitGatewayRegistrationsOutput) ([]*sdp.Item, error) { @@ -45,10 +45,6 @@ func transitGatewayRegistrationOutputMapper(_ context.Context, _ *networkmanager Query: *r.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -63,10 +59,6 @@ func transitGatewayRegistrationOutputMapper(_ context.Context, _ *networkmanager Query: *r.TransitGatewayArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkmanager-transit-gateway-registration_test.go b/aws-source/adapters/networkmanager-transit-gateway-registration_test.go index 36dca8d0..2b0fbeed 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-registration_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-registration_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTransitGatewayRegistrationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go index fd931e1e..45667804 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go +++ b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func getTransitGatewayRouteTableAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.TransitGatewayRouteTableAttachment, error) { @@ -48,10 +48,6 @@ func transitGatewayRouteTableAttachmentItemMapper(_, scope string, awsItem *type Query: *awsItem.Attachment.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -63,10 +59,6 @@ func transitGatewayRouteTableAttachmentItemMapper(_, scope string, awsItem *type Query: *awsItem.PeeringId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -80,10 +72,6 @@ func transitGatewayRouteTableAttachmentItemMapper(_, scope string, awsItem *type Query: *awsItem.TransitGatewayRouteTableArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go index 1c36a9e8..92ada429 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTransitGatewayRouteTableAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-vpc-attachment.go b/aws-source/adapters/networkmanager-vpc-attachment.go index 08e16a9b..1ef60ad9 100644 --- a/aws-source/adapters/networkmanager-vpc-attachment.go +++ b/aws-source/adapters/networkmanager-vpc-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func vpcAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.VpcAttachment, error) { @@ -48,10 +48,6 @@ func vpcAttachmentItemMapper(_, scope string, awsItem *types.VpcAttachment) (*sd Query: *awsItem.Attachment.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-vpc-attachment_test.go b/aws-source/adapters/networkmanager-vpc-attachment_test.go index 222742e4..0e9baa94 100644 --- a/aws-source/adapters/networkmanager-vpc-attachment_test.go +++ b/aws-source/adapters/networkmanager-vpc-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestVPCAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-cluster-parameter-group.go b/aws-source/adapters/rds-db-cluster-parameter-group.go index ca386672..967ad11f 100644 --- a/aws-source/adapters/rds-db-cluster-parameter-group.go +++ b/aws-source/adapters/rds-db-cluster-parameter-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type ClusterParameterGroup struct { diff --git a/aws-source/adapters/rds-db-cluster-parameter-group_test.go b/aws-source/adapters/rds-db-cluster-parameter-group_test.go index 6d89db4d..e02ff713 100644 --- a/aws-source/adapters/rds-db-cluster-parameter-group_test.go +++ b/aws-source/adapters/rds-db-cluster-parameter-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBClusterParameterGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-cluster.go b/aws-source/adapters/rds-db-cluster.go index 8cf90920..40a9adb0 100644 --- a/aws-source/adapters/rds-db-cluster.go +++ b/aws-source/adapters/rds-db-cluster.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeDBClustersInput, output *rds.DescribeDBClustersOutput) ([]*sdp.Item, error) { @@ -50,11 +50,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.DBSubnetGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: false, - }, }) } @@ -67,11 +62,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *endpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always linked - In: true, - Out: true, - }, }) } } @@ -85,11 +75,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: replica, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -103,11 +88,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *member.DBInstanceIdentifier, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -121,12 +101,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *sg.VpcSecurityGroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the security group can affect the cluster - In: true, - // The cluster won't affect the security group - Out: false, - }, }) } } @@ -139,12 +113,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.HostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the hosted zone can affect the cluster - In: true, - // The cluster won't affect the hosted zone - Out: false, - }, }) } @@ -157,12 +125,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.KmsKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the KMS key can affect the cluster - In: true, - // The cluster won't affect the KMS key - Out: false, - }, }) } } @@ -175,12 +137,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.ActivityStreamKinesisStreamName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the Kinesis stream can affect the cluster - In: true, - // Changes to the cluster can affect the Kinesis stream - Out: true, - }, }) } @@ -192,11 +148,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: endpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always linked - In: true, - Out: true, - }, }) } @@ -209,12 +160,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *optionGroup.DBClusterOptionGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the option group can affect the cluster - In: true, - // Changes to the cluster won't affect the option group - Out: false, - }, }) } } @@ -229,12 +174,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.MasterUserSecret.KmsKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the KMS key can affect the cluster - In: true, - // The cluster won't affect the KMS key - Out: false, - }, }) } } @@ -248,12 +187,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.MasterUserSecret.SecretArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the secret can affect the cluster - In: true, - // The cluster won't affect the secret - Out: false, - }, }) } } @@ -268,12 +201,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.MonitoringRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the IAM role can affect the cluster - In: true, - // The cluster won't affect the IAM role - Out: false, - }, }) } } @@ -288,12 +215,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.PerformanceInsightsKMSKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the KMS key can affect the cluster - In: true, - // The cluster won't affect the KMS key - Out: false, - }, }) } } @@ -307,11 +228,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.ReplicationSourceIdentifier, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/rds-db-cluster_test.go b/aws-source/adapters/rds-db-cluster_test.go index c35d57e9..898fdfdc 100644 --- a/aws-source/adapters/rds-db-cluster_test.go +++ b/aws-source/adapters/rds-db-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBClusterOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-instance.go b/aws-source/adapters/rds-db-instance.go index 2b882a57..d0cfac1f 100644 --- a/aws-source/adapters/rds-db-instance.go +++ b/aws-source/adapters/rds-db-instance.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func statusToHealth(status string) *sdp.Health { @@ -128,11 +128,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.Endpoint.Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always links - In: true, - Out: true, - }, }) } @@ -144,12 +139,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.Endpoint.HostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the endpoint - In: true, - // The instance won't affect the hosted zone - Out: false, - }, }) } } @@ -163,12 +152,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *sg.VpcSecurityGroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group can affect the instance - In: true, - // The instance won't affect the security group - Out: false, - }, }) } } @@ -182,12 +165,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *paramGroup.DBParameterGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the parameter group can affect the instance - In: true, - // The instance won't affect the parameter group - Out: false, - }, }) } } @@ -200,12 +177,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *dbSubnetGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet group can affect the instance - In: true, - // The instance won't affect the subnet group - Out: false, - }, }) } @@ -217,11 +188,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.DBClusterIdentifier, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } @@ -235,12 +201,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.KmsKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the KMS key can affect the instance - In: true, - // The instance won't affect the KMS key - Out: false, - }, }) } } @@ -254,11 +214,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.EnhancedMonitoringResourceArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -272,12 +227,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.MonitoringRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the role can affect the instance - In: true, - // The instance won't affect the role - Out: false, - }, }) } } @@ -292,12 +241,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.PerformanceInsightsKMSKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the KMS key can affect the instance - In: true, - // The instance won't affect the KMS key - Out: false, - }, }) } } @@ -312,12 +255,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *role.RoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the role can affect the instance - In: true, - // The instance won't affect the role - Out: false, - }, }) } } @@ -331,11 +268,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.ActivityStreamKinesisStreamName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } @@ -348,11 +280,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.AwsBackupRecoveryPointArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -367,12 +294,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.CustomIamInstanceProfile, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the instance profile can affect the instance - In: true, - // The instance won't affect the instance profile - Out: false, - }, }) } } @@ -387,11 +308,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *replication.DBInstanceAutomatedBackupsArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -406,11 +322,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.ListenerEndpoint.Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always links - In: true, - Out: true, - }, }) } @@ -422,12 +333,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.ListenerEndpoint.HostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the endpoint - In: true, - // The instance won't affect the hosted zone - Out: false, - }, }) } } @@ -441,12 +346,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.MasterUserSecret.KmsKeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the KMS key can affect the instance - In: true, - // The instance won't affect the KMS key - Out: false, - }, }) } @@ -459,12 +358,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.MasterUserSecret.SecretArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret can affect the instance - In: true, - // The instance won't affect the secret - Out: false, - }, }) } } diff --git a/aws-source/adapters/rds-db-instance_test.go b/aws-source/adapters/rds-db-instance_test.go index 6fe35b5b..8ccd1176 100644 --- a/aws-source/adapters/rds-db-instance_test.go +++ b/aws-source/adapters/rds-db-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBInstanceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-parameter-group.go b/aws-source/adapters/rds-db-parameter-group.go index 2aeca89e..ea0d98ea 100644 --- a/aws-source/adapters/rds-db-parameter-group.go +++ b/aws-source/adapters/rds-db-parameter-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type ParameterGroup struct { diff --git a/aws-source/adapters/rds-db-parameter-group_test.go b/aws-source/adapters/rds-db-parameter-group_test.go index 3c3d10e0..e07c9cc9 100644 --- a/aws-source/adapters/rds-db-parameter-group_test.go +++ b/aws-source/adapters/rds-db-parameter-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBParameterGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-subnet-group.go b/aws-source/adapters/rds-db-subnet-group.go index 2f22822a..5f999062 100644 --- a/aws-source/adapters/rds-db-subnet-group.go +++ b/aws-source/adapters/rds-db-subnet-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeDBSubnetGroupsInput, output *rds.DescribeDBSubnetGroupsOutput) ([]*sdp.Item, error) { @@ -50,12 +50,6 @@ func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope stri Query: *sg.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC can affect the subnet group - In: true, - // The subnet group won't affect the VPC - Out: false, - }, }) } @@ -68,12 +62,6 @@ func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope stri Query: *subnet.SubnetIdentifier, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet can affect the subnet group - In: true, - // The subnet group won't affect the subnet - Out: false, - }, }) } @@ -87,12 +75,6 @@ func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope stri Query: *subnet.SubnetOutpost.Arn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the outpost can affect the subnet group - In: true, - // The subnet group won't affect the outpost - Out: false, - }, }) } } diff --git a/aws-source/adapters/rds-db-subnet-group_test.go b/aws-source/adapters/rds-db-subnet-group_test.go index 4c1ac8e8..7b7db1dc 100644 --- a/aws-source/adapters/rds-db-subnet-group_test.go +++ b/aws-source/adapters/rds-db-subnet-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBSubnetGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-option-group.go b/aws-source/adapters/rds-option-group.go index 846eb6f6..37787cd3 100644 --- a/aws-source/adapters/rds-option-group.go +++ b/aws-source/adapters/rds-option-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func optionGroupOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeOptionGroupsInput, output *rds.DescribeOptionGroupsOutput) ([]*sdp.Item, error) { @@ -53,7 +53,7 @@ func NewRDSOptionGroupAdapter(client rdsClient, accountID string, region string, AccountID: accountID, Client: client, AdapterMetadata: optionGroupAdapterMetadata, - cache: cache, + cache: cache, PaginatorBuilder: func(client rdsClient, params *rds.DescribeOptionGroupsInput) Paginator[*rds.DescribeOptionGroupsOutput, *rds.Options] { return rds.NewDescribeOptionGroupsPaginator(client, params) }, diff --git a/aws-source/adapters/route53-health-check.go b/aws-source/adapters/route53-health-check.go index f743e866..93aaf6ec 100644 --- a/aws-source/adapters/route53-health-check.go +++ b/aws-source/adapters/route53-health-check.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type HealthCheck struct { @@ -103,11 +103,6 @@ func healthCheckItemMapper(_, scope string, awsItem *HealthCheck) (*sdp.Item, er Method: sdp.QueryMethod_SEARCH, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/route53-health-check_test.go b/aws-source/adapters/route53-health-check_test.go index 69024a9a..35939600 100644 --- a/aws-source/adapters/route53-health-check_test.go +++ b/aws-source/adapters/route53-health-check_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestHealthCheckItemMapper(t *testing.T) { diff --git a/aws-source/adapters/route53-hosted-zone.go b/aws-source/adapters/route53-hosted-zone.go index bf6f855e..6fc5f882 100644 --- a/aws-source/adapters/route53-hosted-zone.go +++ b/aws-source/adapters/route53-hosted-zone.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func hostedZoneGetFunc(ctx context.Context, client *route53.Client, scope, query string) (*types.HostedZone, error) { @@ -59,12 +59,6 @@ func hostedZoneItemMapper(_, scope string, awsItem *types.HostedZone) (*sdp.Item Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the resource record set - Out: true, - // The resource record set won't affect the hosted zone - In: false, - }, }, }, } diff --git a/aws-source/adapters/route53-hosted-zone_test.go b/aws-source/adapters/route53-hosted-zone_test.go index 322c2da5..da48d7de 100644 --- a/aws-source/adapters/route53-hosted-zone_test.go +++ b/aws-source/adapters/route53-hosted-zone_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestHostedZoneItemMapper(t *testing.T) { diff --git a/aws-source/adapters/route53-resource-record-set.go b/aws-source/adapters/route53-resource-record-set.go index d6ba1d8a..cb9ac5c0 100644 --- a/aws-source/adapters/route53-resource-record-set.go +++ b/aws-source/adapters/route53-resource-record-set.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func resourceRecordSetGetFunc(ctx context.Context, client *route53.Client, scope, query string) (*types.ResourceRecordSet, error) { @@ -125,10 +125,6 @@ func resourceRecordSetItemMapper(_, scope string, awsItem *types.ResourceRecordS Query: recordName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -142,11 +138,6 @@ func resourceRecordSetItemMapper(_, scope string, awsItem *types.ResourceRecordS Query: *awsItem.AliasTarget.DNSName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS aliases links - In: true, - Out: true, - }, }) } } @@ -160,11 +151,6 @@ func resourceRecordSetItemMapper(_, scope string, awsItem *types.ResourceRecordS Query: *record.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS aliases links - In: true, - Out: true, - }, }) } } @@ -177,11 +163,6 @@ func resourceRecordSetItemMapper(_, scope string, awsItem *types.ResourceRecordS Query: *awsItem.HealthCheckId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Health check links tightly - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/route53-resource-record-set_test.go b/aws-source/adapters/route53-resource-record-set_test.go index 6719e02e..d18c15a9 100644 --- a/aws-source/adapters/route53-resource-record-set_test.go +++ b/aws-source/adapters/route53-resource-record-set_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestResourceRecordSetItemMapper(t *testing.T) { diff --git a/aws-source/adapters/s3.go b/aws-source/adapters/s3.go index 46599ed2..2e187d5e 100644 --- a/aws-source/adapters/s3.go +++ b/aws-source/adapters/s3.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) const CacheDuration = 10 * time.Minute @@ -441,11 +441,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: url, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // HTTP always linked - In: true, - Out: true, - }, }) } } @@ -462,11 +457,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *lambdaConfig.LambdaFunctionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -482,11 +472,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *q.QueueArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -502,11 +487,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *topic.TopicArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -521,11 +501,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *bucket.LoggingEnabled.TargetBucket, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -542,11 +517,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *bucket.InventoryConfiguration.Destination.S3BucketDestination.Bucket, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -570,11 +540,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *bucket.AnalyticsConfiguration.StorageClassAnalysis.DataExport.Destination.S3BucketDestination.Bucket, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/s3_test.go b/aws-source/adapters/s3_test.go index b3e23b51..574c87be 100644 --- a/aws-source/adapters/s3_test.go +++ b/aws-source/adapters/s3_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestS3SearchImpl(t *testing.T) { diff --git a/aws-source/adapters/sns-data-protection-policy.go b/aws-source/adapters/sns-data-protection-policy.go index 6a20b0f7..5d54efe7 100644 --- a/aws-source/adapters/sns-data-protection-policy.go +++ b/aws-source/adapters/sns-data-protection-policy.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type dataProtectionPolicyClient interface { @@ -51,14 +51,6 @@ func getDataProtectionPolicyFunc(ctx context.Context, client dataProtectionPolic Query: *input.ResourceArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting the topic will delete the inline policy - In: true, - // Changing policy will affect the topic: - // a new statement denying credit card numbers will make the topic stop delivering messages - // containing credit card numbers - Out: true, - }, }) return item, nil diff --git a/aws-source/adapters/sns-data-protection-policy_test.go b/aws-source/adapters/sns-data-protection-policy_test.go index a1c75dca..3e3fe366 100644 --- a/aws-source/adapters/sns-data-protection-policy_test.go +++ b/aws-source/adapters/sns-data-protection-policy_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type mockDataProtectionPolicyClient struct{} diff --git a/aws-source/adapters/sns-endpoint.go b/aws-source/adapters/sns-endpoint.go index 8bf5c486..0b3b56cf 100644 --- a/aws-source/adapters/sns-endpoint.go +++ b/aws-source/adapters/sns-endpoint.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type endpointClient interface { diff --git a/aws-source/adapters/sns-endpoint_test.go b/aws-source/adapters/sns-endpoint_test.go index e8ddd5b2..b26f5b65 100644 --- a/aws-source/adapters/sns-endpoint_test.go +++ b/aws-source/adapters/sns-endpoint_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type mockEndpointClient struct{} diff --git a/aws-source/adapters/sns-platform-application.go b/aws-source/adapters/sns-platform-application.go index 90354de1..cfd00ead 100644 --- a/aws-source/adapters/sns-platform-application.go +++ b/aws-source/adapters/sns-platform-application.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type platformApplicationClient interface { @@ -57,12 +57,6 @@ func getPlatformApplicationFunc(ctx context.Context, client platformApplicationC Query: *input.PlatformApplicationArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // An unhealthy endpoint won't affect the platform application - In: false, - // If platform application is unhealthy, then endpoints won't get notifications - Out: true, - }, }) return item, nil diff --git a/aws-source/adapters/sns-platform-application_test.go b/aws-source/adapters/sns-platform-application_test.go index 3bc4d2a3..f7fffbd6 100644 --- a/aws-source/adapters/sns-platform-application_test.go +++ b/aws-source/adapters/sns-platform-application_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type mockPlatformApplicationClient struct{} diff --git a/aws-source/adapters/sns-subscription.go b/aws-source/adapters/sns-subscription.go index 3df89e62..eeb67242 100644 --- a/aws-source/adapters/sns-subscription.go +++ b/aws-source/adapters/sns-subscription.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type subsCli interface { @@ -54,12 +54,6 @@ func getSubsFunc(ctx context.Context, client subsCli, scope string, input *sns.G Query: topicArn.(string), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // If topic is not healthy, subscription will not work - In: true, - // Subscription won't affect the topic - Out: false, - }, }) } @@ -72,12 +66,6 @@ func getSubsFunc(ctx context.Context, client subsCli, scope string, input *sns.G Query: arn.ResourceID(), Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // If role is not healthy, subscription will not work - In: true, - // Subscription won't affect the role - Out: false, - }, }) } } diff --git a/aws-source/adapters/sns-subscription_test.go b/aws-source/adapters/sns-subscription_test.go index f3b46f59..d83d419f 100644 --- a/aws-source/adapters/sns-subscription_test.go +++ b/aws-source/adapters/sns-subscription_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type snsTestClient struct{} diff --git a/aws-source/adapters/sns-topic.go b/aws-source/adapters/sns-topic.go index 9bbd8d8b..fd7d4368 100644 --- a/aws-source/adapters/sns-topic.go +++ b/aws-source/adapters/sns-topic.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type topicClient interface { @@ -54,12 +54,6 @@ func getTopicFunc(ctx context.Context, client topicClient, scope string, input * Query: fmt.Sprint(kmsMasterKeyID), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key will affect the topic - In: true, - // Changing the topic won't affect the key - Out: false, - }, }) } diff --git a/aws-source/adapters/sns-topic_test.go b/aws-source/adapters/sns-topic_test.go index 484fd227..57f344de 100644 --- a/aws-source/adapters/sns-topic_test.go +++ b/aws-source/adapters/sns-topic_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type testTopicClient struct{} diff --git a/aws-source/adapters/sqs-queue.go b/aws-source/adapters/sqs-queue.go index a6eccbe2..7283bf7d 100644 --- a/aws-source/adapters/sqs-queue.go +++ b/aws-source/adapters/sqs-queue.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/sqs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type sqsClient interface { @@ -58,10 +58,6 @@ func getFunc(ctx context.Context, client sqsClient, scope string, input *sqs.Get Query: *input.QueueUrl, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -74,12 +70,6 @@ func getFunc(ctx context.Context, client sqsClient, scope string, input *sqs.Get Query: arn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // If event source mappings change, it doesn't affect the queue itself - In: false, - // If the SQS queue is updated, event source mappings will be affected - Out: true, - }, }) } diff --git a/aws-source/adapters/sqs-queue_test.go b/aws-source/adapters/sqs-queue_test.go index 54672448..11524d97 100644 --- a/aws-source/adapters/sqs-queue_test.go +++ b/aws-source/adapters/sqs-queue_test.go @@ -6,8 +6,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/sqs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type testClient struct{} @@ -77,13 +77,6 @@ func TestGetFunc(t *testing.T) { if httpLink.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { t.Errorf("Expected HTTP link method to be SEARCH, got %v", httpLink.GetQuery().GetMethod()) } - // Test HTTP link blast propagation (bidirectional) - if httpLink.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected HTTP link blast propagation In to be true, got %v", httpLink.GetBlastPropagation().GetIn()) - } - if httpLink.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected HTTP link blast propagation Out to be true, got %v", httpLink.GetBlastPropagation().GetOut()) - } // Test Lambda Event Source Mapping link lambdaLink := item.GetLinkedItemQueries()[1] @@ -96,13 +89,6 @@ func TestGetFunc(t *testing.T) { if lambdaLink.GetQuery().GetQuery() != "arn:aws:sqs:us-west-2:123456789012:MyQueue" { t.Errorf("Expected Lambda link query to be the Queue ARN, got %s", lambdaLink.GetQuery().GetQuery()) } - // Test Lambda Event Source Mapping link blast propagation (outgoing only) - if lambdaLink.GetBlastPropagation().GetIn() != false { - t.Errorf("Expected Lambda link blast propagation In to be false, got %v", lambdaLink.GetBlastPropagation().GetIn()) - } - if lambdaLink.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected Lambda link blast propagation Out to be true, got %v", lambdaLink.GetBlastPropagation().GetOut()) - } } func TestSqsQueueSearchInputMapper(t *testing.T) { diff --git a/aws-source/adapters/ssm-parameter.go b/aws-source/adapters/ssm-parameter.go index ea8b5a32..0294d5a3 100644 --- a/aws-source/adapters/ssm-parameter.go +++ b/aws-source/adapters/ssm-parameter.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/sourcegraph/conc/iter" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/aws-source/adapters/ssm-parameter_test.go b/aws-source/adapters/ssm-parameter_test.go index 2c01d3b7..97e102f8 100644 --- a/aws-source/adapters/ssm-parameter_test.go +++ b/aws-source/adapters/ssm-parameter_test.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" ) type mockSSMClient struct{} diff --git a/aws-source/build/package/Dockerfile b/aws-source/build/package/Dockerfile index 204bdfa6..5ea83d03 100644 --- a/aws-source/build/package/Dockerfile +++ b/aws-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source aws-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source aws-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/aws-source/cmd/root.go b/aws-source/cmd/root.go index 0a9a4f11..59f6e1d5 100644 --- a/aws-source/cmd/root.go +++ b/aws-source/cmd/root.go @@ -10,9 +10,9 @@ import ( "github.com/getsentry/sentry-go" "github.com/overmindtech/cli/aws-source/proc" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/logging" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/logging" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/aws-source/module/.cursor/BUGBOT.md b/aws-source/module/.cursor/BUGBOT.md new file mode 100644 index 00000000..08d7fca8 --- /dev/null +++ b/aws-source/module/.cursor/BUGBOT.md @@ -0,0 +1,16 @@ +# Terraform Module Review Rules + +## HCL: IAM policy must stay read-only + +If any changed `.tf` file modifies an IAM policy statement's `Action` list: + +- Verify every action uses only read-only prefixes: `Get*`, `Describe*`, `List*`, `GetBucket*`, `ListAllMyBuckets`, `ListTagsForResource`, `GetMetricData`. +- Add a blocking Bug titled "IAM policy contains write actions" if any action allows mutation (e.g., `Put*`, `Create*`, `Delete*`, `Update*`, `Attach*`, `Detach*`). +- Body: "The Overmind IAM role must be strictly read-only. Write actions violate customer trust policies and the principle of least privilege. Remove the offending actions." + +## Provider Go: Use diag.Diagnostics for errors + +If any changed `.go` file in `provider/` returns an error from a resource or data source CRUD function using bare `fmt.Errorf` or `errors.New`: + +- Add a warning titled "Use diag.Diagnostics instead of bare errors" +- Body: "Terraform provider resource and data source functions should return errors via `diag.Diagnostics` (e.g., `diag.FromErr(err)`) so that Terraform can display structured error output to users. See the [Terraform Plugin Framework documentation](https://developer.hashicorp.com/terraform/plugin/framework/diagnostics) for guidance." diff --git a/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml b/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml new file mode 100644 index 00000000..3a59a73a --- /dev/null +++ b/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml @@ -0,0 +1,112 @@ +name: Finalize Copybara Sync + +on: + push: + branches: + - 'copybara/v*' + +concurrency: + group: copybara-sync-${{ github.ref }} + cancel-in-progress: true + +jobs: + finalize: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Extract version from branch name + id: version + run: | + VERSION=$(echo "$GITHUB_REF" | sed 's|refs/heads/copybara/||') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v6 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "actions@github.com" + + - name: Run go mod tidy + run: go mod tidy + + - name: Commit and push go mod tidy changes + run: | + if ! git diff --quiet go.mod go.sum; then + git add go.mod go.sum + git commit -m "Run go mod tidy" + git push origin ${{ github.ref_name }} + else + echo "No changes from go mod tidy" + fi + + - name: Extract original commit author + id: author + run: | + AUTHOR_EMAIL=$(git log -1 --format='%ae' --author='^(?!.*actions@github.com)' --perl-regexp 2>/dev/null || git log -1 --format='%ae') + AUTHOR_NAME=$(git log -1 --format='%an' --author='^(?!.*actions@github.com)' --perl-regexp 2>/dev/null || git log -1 --format='%an') + echo "email=$AUTHOR_EMAIL" >> $GITHUB_OUTPUT + echo "name=$AUTHOR_NAME" >> $GITHUB_OUTPUT + + if [[ "$AUTHOR_EMAIL" =~ ^([^@]+)@users\.noreply\.github\.com$ ]]; then + GITHUB_USER=$(echo "${BASH_REMATCH[1]}" | sed 's/^[0-9]*+//') + echo "github_user=$GITHUB_USER" >> $GITHUB_OUTPUT + else + echo "github_user=" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + AUTHOR_NAME: ${{ steps.author.outputs.name }} + AUTHOR_EMAIL: ${{ steps.author.outputs.email }} + GITHUB_USER: ${{ steps.author.outputs.github_user }} + run: | + PR_BODY="## Copybara Sync - Release ${VERSION} + + This PR was automatically created by Copybara, syncing changes from the [overmindtech/workspace](https://github.com/overmindtech/workspace) monorepo. + + **Original author:** ${AUTHOR_NAME} (${AUTHOR_EMAIL}) + + ### What happens when this PR is merged? + + 1. The \`tag-on-merge\` workflow will automatically create the \`${VERSION}\` tag on main + 2. This tag will trigger the release workflow, which will: + - Build provider binaries for all platforms via GoReleaser + - Sign checksums with GPG + - Create a GitHub release + - Terraform Registry will detect the release and publish the provider + + ### Review Checklist + + - [ ] Changes look correct and match the expected monorepo sync + - [ ] CI checks pass + " + + PR_URL=$(gh pr create \ + --base main \ + --head "${{ github.ref_name }}" \ + --title "Release ${VERSION}" \ + --body "$PR_BODY") + + echo "Created PR: $PR_URL" + + if [ -n "$GITHUB_USER" ]; then + echo "Requesting review from original author: $GITHUB_USER" + gh pr edit "$PR_URL" --add-reviewer "$GITHUB_USER" || true + fi + + echo "Requesting review from Engineering team" + gh pr edit "$PR_URL" --add-reviewer "overmindtech/Engineering" || true diff --git a/aws-source/module/provider/.github/workflows/release.yml b/aws-source/module/provider/.github/workflows/release.yml new file mode 100644 index 00000000..130f5746 --- /dev/null +++ b/aws-source/module/provider/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: depot-ubuntu-24.04-8 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v2 + + - name: Load GPG secrets from 1Password + uses: 1password/load-secrets-action@v3 + with: + export-env: true + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_RO_TOKEN }} + GPG_PRIVATE_KEY: 'op://global/Terraform Provider GPG Key/private-key' + PASSPHRASE: 'op://global/Terraform Provider GPG Key/passphrase' + GPG_FINGERPRINT: 'op://global/Terraform Provider GPG Key/fingerprint' + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + id: import_gpg + with: + gpg_private_key: ${{ env.GPG_PRIVATE_KEY }} + passphrase: ${{ env.PASSPHRASE }} + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/aws-source/module/provider/.github/workflows/tag-on-merge.yml b/aws-source/module/provider/.github/workflows/tag-on-merge.yml new file mode 100644 index 00000000..800ccb96 --- /dev/null +++ b/aws-source/module/provider/.github/workflows/tag-on-merge.yml @@ -0,0 +1,52 @@ +name: Tag Release on Merge + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + tag-release: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'copybara/v') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Extract version from branch name + id: version + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + VERSION=$(echo "$BRANCH" | sed 's|copybara/||') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.RELEASE_PAT }} + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "actions@github.com" + + - name: Create and push tag + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + echo "Creating tag: $VERSION" + git tag "$VERSION" + git push origin "$VERSION" + echo "Successfully pushed tag $VERSION" + + - name: Delete copybara branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + echo "Deleting branch: $BRANCH" + git push origin --delete "$BRANCH" || echo "Branch may have already been deleted" diff --git a/aws-source/module/provider/.goreleaser.yml b/aws-source/module/provider/.goreleaser.yml new file mode 100644 index 00000000..4aa546e3 --- /dev/null +++ b/aws-source/module/provider/.goreleaser.yml @@ -0,0 +1,56 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +version: 2 + +builds: + - binary: "{{ .ProjectName }}_v{{ .Version }}" + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -s -w -X main.version={{ .Version }} + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + - "386" + ignore: + - goos: darwin + goarch: "386" + +archives: + - formats: [zip] + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" + algorithm: sha256 + extra_files: + - glob: terraform-registry-manifest.json + name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" + +signs: + - artifacts: checksum + args: + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" + +release: + extra_files: + - glob: terraform-registry-manifest.json + name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/aws-source/module/provider/LICENSE b/aws-source/module/provider/LICENSE new file mode 100644 index 00000000..bc298f07 --- /dev/null +++ b/aws-source/module/provider/LICENSE @@ -0,0 +1,105 @@ +# Functional Source License, Version 1.1, Apache 2.0 Future License + +## Abbreviation + +FSL-1.1-Apache-2.0 + +## Notice + +Copyright 2024 Overmind Technology Inc. + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/aws-source/module/provider/datasource_aws_external_id.go b/aws-source/module/provider/datasource_aws_external_id.go new file mode 100644 index 00000000..cf5f1a0b --- /dev/null +++ b/aws-source/module/provider/datasource_aws_external_id.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + "github.com/hashicorp/terraform-plugin-framework/datasource" + dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + sdp "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +var _ datasource.DataSource = (*awsExternalIdDataSource)(nil) + +type awsExternalIdDataSource struct { + mgmt sdpconnect.ManagementServiceClient +} + +type awsExternalIdDataSourceModel struct { + ExternalID types.String `tfsdk:"external_id"` +} + +func NewAWSExternalIdDataSource() datasource.DataSource { + return &awsExternalIdDataSource{} +} + +func (d *awsExternalIdDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_aws_external_id" +} + +func (d *awsExternalIdDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = dsschema.Schema{ + Description: "Retrieves the stable AWS STS external ID for the current Overmind account. " + + "Use this to configure the trust policy on an IAM role before creating the source.", + Attributes: map[string]dsschema.Attribute{ + "external_id": dsschema.StringAttribute{ + Description: "AWS STS external ID, stable per Overmind account.", + Computed: true, + }, + }, + } +} + +func (d *awsExternalIdDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + mgmt, ok := req.ProviderData.(sdpconnect.ManagementServiceClient) + if !ok { + resp.Diagnostics.AddError("Unexpected DataSource Configure Type", + fmt.Sprintf("Expected sdpconnect.ManagementServiceClient, got %T", req.ProviderData)) + return + } + d.mgmt = mgmt +} + +func (d *awsExternalIdDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSExternalId Read") + defer span.End() + + extIDResp, err := d.mgmt.GetOrCreateAWSExternalId(ctx, + connect.NewRequest(&sdp.GetOrCreateAWSExternalIdRequest{})) + if err != nil { + resp.Diagnostics.AddError("Failed to get AWS external ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "GetOrCreateAWSExternalId failed") + return + } + + externalID := extIDResp.Msg.GetAwsExternalId() + span.SetAttributes(attribute.String("ovm.externalId", externalID)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &awsExternalIdDataSourceModel{ + ExternalID: types.StringValue(externalID), + })...) +} diff --git a/aws-source/module/provider/main.go b/aws-source/module/provider/main.go new file mode 100644 index 00000000..c7e80f55 --- /dev/null +++ b/aws-source/module/provider/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "github.com/uptrace/opentelemetry-go-extra/otellogrus" +) + +var version = "dev" //nolint:gochecknoglobals // injected by GoReleaser ldflags + +const defaultHoneycombAPIKey = "hcaik_01j03qe0exnn2jxpj2vxkqb7yrqtr083kyk9rxxt2wzjamz8be94znqmwa" //nolint:gosec // public ingest key, same as CLI + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + //nolint:gocritic // os.Exit in main after deferred cleanup is the only option + os.Exit(1) + } +} + +func run() error { + formatter := new(log.TextFormatter) + formatter.DisableTimestamp = true + log.SetFormatter(formatter) + log.SetOutput(os.Stderr) + log.SetLevel(log.ErrorLevel) + + honeycombAPIKey := defaultHoneycombAPIKey + if v, ok := os.LookupEnv("HONEYCOMB_API_KEY"); ok { + honeycombAPIKey = v + } + if honeycombAPIKey != "" { + if err := tracing.InitTracerWithUpstreams("overmind-terraform-provider", honeycombAPIKey, ""); err != nil { + return fmt.Errorf("initialising tracing: %w", err) + } + defer tracing.ShutdownTracer(context.Background()) + + log.AddHook(otellogrus.NewHook(otellogrus.WithLevels( + log.AllLevels[:log.GetLevel()+1]..., + ))) + } + + return providerserver.Serve(context.Background(), NewProvider(version), providerserver.ServeOpts{ + Address: "registry.terraform.io/overmindtech/overmind", + }) +} diff --git a/aws-source/module/provider/provider.go b/aws-source/module/provider/provider.go new file mode 100644 index 00000000..a228fe3b --- /dev/null +++ b/aws-source/module/provider/provider.go @@ -0,0 +1,132 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/overmindtech/cli/go/auth" + sdp "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "golang.org/x/oauth2" +) + +var _ provider.Provider = (*overmindProvider)(nil) + +type overmindProvider struct { + version string +} + +type overmindProviderModel struct { + AppURL types.String `tfsdk:"app_url"` + APIKey types.String `tfsdk:"api_key"` +} + +func NewProvider(version string) func() provider.Provider { + return func() provider.Provider { + return &overmindProvider{version: version} + } +} + +func (p *overmindProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "overmind" + resp.Version = p.version +} + +func (p *overmindProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "The Overmind provider manages infrastructure sources via the Overmind API. " + + "Configuration is read from the OVERMIND_API_KEY and OVERMIND_APP_URL environment variables by default.", + Attributes: map[string]schema.Attribute{ + "api_key": schema.StringAttribute{ + Description: "Overmind API key. Can also be set via the OVERMIND_API_KEY environment variable.", + Optional: true, + Sensitive: true, + }, + "app_url": schema.StringAttribute{ + Description: "Overmind application URL (e.g. https://app.overmind.tech). " + + "Can also be set via the OVERMIND_APP_URL environment variable.", + Optional: true, + }, + }, + } +} + +func (p *overmindProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + ctx, span := tracing.Tracer().Start(ctx, "Provider Configure") + defer span.End() + + var config overmindProviderModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + apiKey := os.Getenv("OVERMIND_API_KEY") + if !config.APIKey.IsNull() { + apiKey = config.APIKey.ValueString() + } + + appURL := os.Getenv("OVERMIND_APP_URL") + if !config.AppURL.IsNull() { + appURL = config.AppURL.ValueString() + } + if appURL == "" { + appURL = "https://app.overmind.tech" + } + + span.SetAttributes(attribute.String("ovm.provider.appUrl", appURL)) + + if apiKey == "" { + resp.Diagnostics.AddError( + "Missing API Key", + "An Overmind API key must be provided via the api_key provider attribute or the OVERMIND_API_KEY environment variable.", + ) + span.SetStatus(codes.Error, "missing API key") + return + } + + oi, err := sdp.NewOvermindInstance(ctx, appURL) + if err != nil { + resp.Diagnostics.AddError("Failed to resolve Overmind instance", + fmt.Sprintf("Could not resolve instance data from %s: %s", appURL, err)) + span.RecordError(err) + span.SetStatus(codes.Error, "instance resolution failed") + return + } + + apiURL := oi.ApiUrl.String() + span.SetAttributes(attribute.String("ovm.provider.apiUrl", apiURL)) + + tokenSource := auth.NewAPIKeyTokenSource(apiKey, apiURL) + httpClient := tracing.HTTPClient() + httpClient.Transport = &oauth2.Transport{ + Source: tokenSource, + Base: httpClient.Transport, + } + + mgmtClient := sdpconnect.NewManagementServiceClient(httpClient, apiURL) + + resp.DataSourceData = mgmtClient + resp.ResourceData = mgmtClient +} + +func (p *overmindProvider) Resources(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + NewAWSSourceResource, + } +} + +func (p *overmindProvider) DataSources(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + NewAWSExternalIdDataSource, + } +} diff --git a/aws-source/module/provider/provider_test.go b/aws-source/module/provider/provider_test.go new file mode 100644 index 00000000..db9ca1d8 --- /dev/null +++ b/aws-source/module/provider/provider_test.go @@ -0,0 +1,243 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "regexp" + "sync" + "testing" + + "connectrpc.com/connect" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + tfresource "github.com/hashicorp/terraform-plugin-testing/helper/resource" + sdp "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "golang.org/x/oauth2" +) + +// --- mock ManagementService handler --- + +type mockMgmtHandler struct { + sdpconnect.UnimplementedManagementServiceHandler + mu sync.Mutex + sources map[string]*sdp.Source + externalID string +} + +func newMockMgmtHandler() *mockMgmtHandler { + return &mockMgmtHandler{ + sources: make(map[string]*sdp.Source), + externalID: "test-external-id-12345", + } +} + +func (m *mockMgmtHandler) GetOrCreateAWSExternalId(_ context.Context, _ *connect.Request[sdp.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp.GetOrCreateAWSExternalIdResponse], error) { + return connect.NewResponse(&sdp.GetOrCreateAWSExternalIdResponse{ + AwsExternalId: m.externalID, + }), nil +} + +func (m *mockMgmtHandler) CreateSource(_ context.Context, req *connect.Request[sdp.CreateSourceRequest]) (*connect.Response[sdp.CreateSourceResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + id := uuid.New() + source := &sdp.Source{ + Metadata: &sdp.SourceMetadata{UUID: id[:]}, + Properties: req.Msg.GetProperties(), + } + m.sources[id.String()] = source + return connect.NewResponse(&sdp.CreateSourceResponse{Source: source}), nil +} + +func (m *mockMgmtHandler) GetSource(_ context.Context, req *connect.Request[sdp.GetSourceRequest]) (*connect.Response[sdp.GetSourceResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + id, err := uuid.FromBytes(req.Msg.GetUUID()) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + source, ok := m.sources[id.String()] + if !ok { + return nil, connect.NewError(connect.CodeNotFound, nil) + } + return connect.NewResponse(&sdp.GetSourceResponse{Source: source}), nil +} + +func (m *mockMgmtHandler) UpdateSource(_ context.Context, req *connect.Request[sdp.UpdateSourceRequest]) (*connect.Response[sdp.UpdateSourceResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + id, err := uuid.FromBytes(req.Msg.GetUUID()) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + source, ok := m.sources[id.String()] + if !ok { + return nil, connect.NewError(connect.CodeNotFound, nil) + } + source.Properties = req.Msg.GetProperties() + return connect.NewResponse(&sdp.UpdateSourceResponse{Source: source}), nil +} + +func (m *mockMgmtHandler) DeleteSource(_ context.Context, req *connect.Request[sdp.DeleteSourceRequest]) (*connect.Response[sdp.DeleteSourceResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + id, err := uuid.FromBytes(req.Msg.GetUUID()) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + if _, ok := m.sources[id.String()]; !ok { + return nil, connect.NewError(connect.CodeNotFound, nil) + } + delete(m.sources, id.String()) + return connect.NewResponse(&sdp.DeleteSourceResponse{}), nil +} + +// --- test provider that bypasses auth --- + +// testProvider wraps the real provider but overrides Configure to inject a +// pre-built client backed by the mock server. This avoids needing the +// instance-data endpoint, ApiKeyService, or real JWTs in unit tests. +type testProvider struct { + overmindProvider + serverURL string +} + +var _ provider.Provider = (*testProvider)(nil) + +func (p *testProvider) Configure(ctx context.Context, _ provider.ConfigureRequest, resp *provider.ConfigureResponse) { + httpClient := oauth2.NewClient(ctx, + oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "test"})) + mgmtClient := sdpconnect.NewManagementServiceClient(httpClient, p.serverURL) + resp.DataSourceData = mgmtClient + resp.ResourceData = mgmtClient +} + +func (p *testProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{}, + } +} + +func (p *testProvider) Resources(ctx context.Context) []func() resource.Resource { + return p.overmindProvider.Resources(ctx) +} + +func (p *testProvider) DataSources(ctx context.Context) []func() datasource.DataSource { + return p.overmindProvider.DataSources(ctx) +} + +// --- test helpers --- + +func startTestServer(t *testing.T) string { + t.Helper() + handler := newMockMgmtHandler() + path, h := sdpconnect.NewManagementServiceHandler(handler) + mux := http.NewServeMux() + mux.Handle(path, h) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv.URL +} + +func unitTestProviderFactories(serverURL string) map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "overmind": providerserver.NewProtocol6WithError(&testProvider{ + overmindProvider: overmindProvider{version: "test"}, + serverURL: serverURL, + }), + } +} + +func accTestProviderFactories() map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "overmind": providerserver.NewProtocol6WithError(NewProvider("test")()), + } +} + +// --- unit tests (mock server, always run) --- + +func TestAWSSourceResource_CRUD(t *testing.T) { + serverURL := startTestServer(t) + + tfresource.UnitTest(t, tfresource.TestCase{ + ProtoV6ProviderFactories: unitTestProviderFactories(serverURL), + Steps: []tfresource.TestStep{ + { + Config: testAccAWSSourceConfig("test-source", "arn:aws:iam::123456789012:role/test", `["us-east-1", "eu-west-1"]`), + Check: tfresource.ComposeAggregateTestCheckFunc( + tfresource.TestCheckResourceAttrSet("overmind_aws_source.test", "id"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "name", "test-source"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "aws_role_arn", "arn:aws:iam::123456789012:role/test"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "external_id", "test-external-id-12345"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "aws_regions.#", "2"), + ), + }, + { + Config: testAccAWSSourceConfig("updated-source", "arn:aws:iam::123456789012:role/test", `["us-west-2"]`), + Check: tfresource.ComposeAggregateTestCheckFunc( + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "name", "updated-source"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "aws_regions.#", "1"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "aws_regions.0", "us-west-2"), + ), + }, + { + ResourceName: "overmind_aws_source.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestProviderConfigure_MissingAPIKey(t *testing.T) { + t.Setenv("OVERMIND_API_KEY", "") + t.Setenv("OVERMIND_APP_URL", "") + + tfresource.UnitTest(t, tfresource.TestCase{ + ProtoV6ProviderFactories: accTestProviderFactories(), + Steps: []tfresource.TestStep{ + { + Config: ` +resource "overmind_aws_source" "test" { + name = "x" + aws_role_arn = "arn" + aws_regions = ["us-east-1"] +} +`, + ExpectError: regexp.MustCompile(`Missing API Key`), + }, + }, + }) +} + +func TestAWSExternalIdDataSource_Read(t *testing.T) { + serverURL := startTestServer(t) + + tfresource.UnitTest(t, tfresource.TestCase{ + ProtoV6ProviderFactories: unitTestProviderFactories(serverURL), + Steps: []tfresource.TestStep{ + { + Config: `data "overmind_aws_external_id" "test" {}`, + Check: tfresource.ComposeAggregateTestCheckFunc( + tfresource.TestCheckResourceAttr( + "data.overmind_aws_external_id.test", "external_id", "test-external-id-12345"), + ), + }, + }, + }) +} + +func testAccAWSSourceConfig(name, roleARN, regions string) string { + return `resource "overmind_aws_source" "test" { + name = "` + name + `" + aws_role_arn = "` + roleARN + `" + aws_regions = ` + regions + ` +}` +} diff --git a/aws-source/module/provider/resource_aws_source.go b/aws-source/module/provider/resource_aws_source.go new file mode 100644 index 00000000..a7d80e24 --- /dev/null +++ b/aws-source/module/provider/resource_aws_source.go @@ -0,0 +1,383 @@ +package main + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + sdp "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "google.golang.org/protobuf/types/known/structpb" +) + +var ( + _ resource.Resource = (*awsSourceResource)(nil) + _ resource.ResourceWithImportState = (*awsSourceResource)(nil) +) + +type awsSourceResource struct { + mgmt sdpconnect.ManagementServiceClient +} + +type awsSourceResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + AWSRoleARN types.String `tfsdk:"aws_role_arn"` + AWSRegions types.List `tfsdk:"aws_regions"` + ExternalID types.String `tfsdk:"external_id"` +} + +func NewAWSSourceResource() resource.Resource { + return &awsSourceResource{} +} + +func (r *awsSourceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_aws_source" +} + +func (r *awsSourceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages an Overmind AWS infrastructure source.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Source UUID assigned by the Overmind API.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "Human-readable name for this source.", + Required: true, + }, + "aws_role_arn": schema.StringAttribute{ + Description: "ARN of the IAM role to assume in the customer's AWS account.", + Required: true, + }, + "aws_regions": schema.ListAttribute{ + Description: "AWS regions this source should discover resources in.", + Required: true, + ElementType: types.StringType, + }, + "external_id": schema.StringAttribute{ + Description: "AWS STS external ID for the IAM trust policy, stable per Overmind account.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *awsSourceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + mgmt, ok := req.ProviderData.(sdpconnect.ManagementServiceClient) + if !ok { + resp.Diagnostics.AddError("Unexpected Resource Configure Type", + fmt.Sprintf("Expected sdpconnect.ManagementServiceClient, got %T", req.ProviderData)) + return + } + r.mgmt = mgmt +} + +func (r *awsSourceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Create") + defer span.End() + + var plan awsSourceResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + span.SetAttributes( + attribute.String("ovm.source.name", plan.Name.ValueString()), + attribute.String("ovm.source.roleArn", plan.AWSRoleARN.ValueString()), + ) + + extIDResp, err := r.mgmt.GetOrCreateAWSExternalId(ctx, + connect.NewRequest(&sdp.GetOrCreateAWSExternalIdRequest{})) + if err != nil { + resp.Diagnostics.AddError("Failed to get AWS external ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "GetOrCreateAWSExternalId failed") + return + } + externalID := extIDResp.Msg.GetAwsExternalId() + + regions, diags := regionsFromList(ctx, plan.AWSRegions) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + sourceConfig, err := structpb.NewStruct(map[string]any{ + "aws-access-strategy": "external-id", + "aws-external-id": externalID, + "aws-target-role-arn": plan.AWSRoleARN.ValueString(), + "aws-regions": toAnySlice(regions), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to build source config", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "config build failed") + return + } + + createResp, err := r.mgmt.CreateSource(ctx, connect.NewRequest(&sdp.CreateSourceRequest{ + Properties: &sdp.SourceProperties{ + DescriptiveName: plan.Name.ValueString(), + Type: "aws", + Config: sourceConfig, + }, + })) + if err != nil { + resp.Diagnostics.AddError("Failed to create source", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "CreateSource failed") + return + } + + source := createResp.Msg.GetSource() + sourceUUID, err := uuid.FromBytes(source.GetMetadata().GetUUID()) + if err != nil { + resp.Diagnostics.AddError("Failed to parse source UUID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "UUID parse failed") + return + } + + plan.ID = types.StringValue(sourceUUID.String()) + plan.ExternalID = types.StringValue(externalID) + + span.SetAttributes(attribute.String("ovm.source.id", sourceUUID.String())) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *awsSourceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Read") + defer span.End() + + var state awsSourceResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + span.SetAttributes(attribute.String("ovm.source.id", state.ID.ValueString())) + + uuidBytes, err := uuidToBytes(state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid source ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "invalid UUID") + return + } + + getResp, err := r.mgmt.GetSource(ctx, connect.NewRequest(&sdp.GetSourceRequest{ + UUID: uuidBytes, + })) + if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + span.SetAttributes(attribute.Bool("ovm.source.removed", true)) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Failed to read source", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "GetSource failed") + return + } + + source := getResp.Msg.GetSource() + props := source.GetProperties() + + state.Name = types.StringValue(props.GetDescriptiveName()) + + if cfg := props.GetConfig(); cfg != nil { + fields := cfg.GetFields() + if v, ok := fields["aws-target-role-arn"]; ok { + state.AWSRoleARN = types.StringValue(v.GetStringValue()) + } + if v, ok := fields["aws-regions"]; ok { + regionVals := regionsFromStructValue(v) + listVal, diags := types.ListValueFrom(ctx, types.StringType, regionVals) + resp.Diagnostics.Append(diags...) + state.AWSRegions = listVal + } + if v, ok := fields["aws-external-id"]; ok { + state.ExternalID = types.StringValue(v.GetStringValue()) + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *awsSourceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Update") + defer span.End() + + var plan awsSourceResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var state awsSourceResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + span.SetAttributes( + attribute.String("ovm.source.id", state.ID.ValueString()), + attribute.String("ovm.source.name", plan.Name.ValueString()), + ) + + uuidBytes, err := uuidToBytes(state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid source ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "invalid UUID") + return + } + + regions, diags := regionsFromList(ctx, plan.AWSRegions) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + externalID := state.ExternalID.ValueString() + + sourceConfig, err := structpb.NewStruct(map[string]any{ + "aws-access-strategy": "external-id", + "aws-external-id": externalID, + "aws-target-role-arn": plan.AWSRoleARN.ValueString(), + "aws-regions": toAnySlice(regions), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to build source config", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "config build failed") + return + } + + _, err = r.mgmt.UpdateSource(ctx, connect.NewRequest(&sdp.UpdateSourceRequest{ + UUID: uuidBytes, + Properties: &sdp.SourceProperties{ + DescriptiveName: plan.Name.ValueString(), + Type: "aws", + Config: sourceConfig, + }, + })) + if err != nil { + resp.Diagnostics.AddError("Failed to update source", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "UpdateSource failed") + return + } + + plan.ID = state.ID + plan.ExternalID = state.ExternalID + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *awsSourceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Delete") + defer span.End() + + var state awsSourceResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + span.SetAttributes(attribute.String("ovm.source.id", state.ID.ValueString())) + + uuidBytes, err := uuidToBytes(state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid source ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "invalid UUID") + return + } + + _, err = r.mgmt.DeleteSource(ctx, connect.NewRequest(&sdp.DeleteSourceRequest{ + UUID: uuidBytes, + })) + if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + span.SetAttributes(attribute.Bool("ovm.source.alreadyGone", true)) + return + } + resp.Diagnostics.AddError("Failed to delete source", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "DeleteSource failed") + } +} + +func (r *awsSourceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Import") + defer span.End() + + span.SetAttributes(attribute.String("ovm.source.id", req.ID)) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// --- helpers --- + +func uuidToBytes(s string) ([]byte, error) { + parsed, err := uuid.Parse(s) + if err != nil { + return nil, fmt.Errorf("parsing UUID %q: %w", s, err) + } + b := parsed[:] + return b, nil +} + +func regionsFromList(ctx context.Context, list types.List) ([]string, diag.Diagnostics) { + var regions []string + diags := list.ElementsAs(ctx, ®ions, false) + return regions, diags +} + +func toAnySlice(ss []string) []any { + out := make([]any, len(ss)) + for i, s := range ss { + out[i] = s + } + return out +} + +func regionsFromStructValue(v *structpb.Value) []string { + lv := v.GetListValue() + if lv == nil { + return []string{} + } + vals := lv.GetValues() + out := make([]string, 0, len(vals)) + for _, item := range vals { + if s := item.GetStringValue(); s != "" { + out = append(out, s) + } + } + return out +} diff --git a/aws-source/module/provider/terraform-registry-manifest.json b/aws-source/module/provider/terraform-registry-manifest.json new file mode 100644 index 00000000..3ffa7837 --- /dev/null +++ b/aws-source/module/provider/terraform-registry-manifest.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "metadata": { + "protocol_versions": [ + "6.0" + ] + } +} diff --git a/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml b/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml new file mode 100644 index 00000000..e6c0f5e0 --- /dev/null +++ b/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml @@ -0,0 +1,84 @@ +name: Finalize Copybara Sync + +on: + push: + branches: + - 'copybara/v*' + +concurrency: + group: copybara-sync-${{ github.ref }} + cancel-in-progress: true + +jobs: + finalize: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Extract version from branch name + id: version + run: | + VERSION=$(echo "$GITHUB_REF" | sed 's|refs/heads/copybara/||') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v6 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + + - name: Extract original commit author + id: author + run: | + AUTHOR_EMAIL=$(git log -1 --format='%ae' --author='^(?!.*actions@github.com)' --perl-regexp 2>/dev/null || git log -1 --format='%ae') + AUTHOR_NAME=$(git log -1 --format='%an' --author='^(?!.*actions@github.com)' --perl-regexp 2>/dev/null || git log -1 --format='%an') + echo "email=$AUTHOR_EMAIL" >> $GITHUB_OUTPUT + echo "name=$AUTHOR_NAME" >> $GITHUB_OUTPUT + + if [[ "$AUTHOR_EMAIL" =~ ^([^@]+)@users\.noreply\.github\.com$ ]]; then + GITHUB_USER=$(echo "${BASH_REMATCH[1]}" | sed 's/^[0-9]*+//') + echo "github_user=$GITHUB_USER" >> $GITHUB_OUTPUT + else + echo "github_user=" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + AUTHOR_NAME: ${{ steps.author.outputs.name }} + AUTHOR_EMAIL: ${{ steps.author.outputs.email }} + GITHUB_USER: ${{ steps.author.outputs.github_user }} + run: | + PR_BODY="## Copybara Sync - Release ${VERSION} + + This PR was automatically created by Copybara, syncing changes from the [overmindtech/workspace](https://github.com/overmindtech/workspace) monorepo. + + **Original author:** ${AUTHOR_NAME} (${AUTHOR_EMAIL}) + + ### What happens when this PR is merged? + + 1. The \`tag-on-merge\` workflow will automatically create the \`${VERSION}\` tag on main + 2. Terraform Registry will detect the tag via webhook and publish the module + + ### Review Checklist + + - [ ] Changes look correct and match the expected monorepo sync + " + + PR_URL=$(gh pr create \ + --base main \ + --head "${{ github.ref_name }}" \ + --title "Release ${VERSION}" \ + --body "$PR_BODY") + + echo "Created PR: $PR_URL" + + if [ -n "$GITHUB_USER" ]; then + echo "Requesting review from original author: $GITHUB_USER" + gh pr edit "$PR_URL" --add-reviewer "$GITHUB_USER" || true + fi + + echo "Requesting review from Engineering team" + gh pr edit "$PR_URL" --add-reviewer "overmindtech/Engineering" || true diff --git a/aws-source/module/terraform/.github/workflows/tag-on-merge.yml b/aws-source/module/terraform/.github/workflows/tag-on-merge.yml new file mode 100644 index 00000000..800ccb96 --- /dev/null +++ b/aws-source/module/terraform/.github/workflows/tag-on-merge.yml @@ -0,0 +1,52 @@ +name: Tag Release on Merge + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + tag-release: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'copybara/v') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Extract version from branch name + id: version + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + VERSION=$(echo "$BRANCH" | sed 's|copybara/||') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.RELEASE_PAT }} + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "actions@github.com" + + - name: Create and push tag + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + echo "Creating tag: $VERSION" + git tag "$VERSION" + git push origin "$VERSION" + echo "Successfully pushed tag $VERSION" + + - name: Delete copybara branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + echo "Deleting branch: $BRANCH" + git push origin --delete "$BRANCH" || echo "Branch may have already been deleted" diff --git a/aws-source/module/terraform/LICENSE b/aws-source/module/terraform/LICENSE new file mode 100644 index 00000000..bc298f07 --- /dev/null +++ b/aws-source/module/terraform/LICENSE @@ -0,0 +1,105 @@ +# Functional Source License, Version 1.1, Apache 2.0 Future License + +## Abbreviation + +FSL-1.1-Apache-2.0 + +## Notice + +Copyright 2024 Overmind Technology Inc. + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/aws-source/module/terraform/README.md b/aws-source/module/terraform/README.md new file mode 100644 index 00000000..65bb91af --- /dev/null +++ b/aws-source/module/terraform/README.md @@ -0,0 +1,120 @@ +# Overmind AWS Source Setup + +Terraform module that configures an AWS account for +[Overmind](https://overmind.tech) infrastructure discovery. A single +`terraform apply` creates: + +1. An IAM role with a read-only policy in the target AWS account +2. A trust policy allowing Overmind to assume the role via STS external ID +3. An Overmind source registration pointing at the role + +## Usage + +```hcl +provider "overmind" {} + +provider "aws" { + region = "us-east-1" +} + +module "overmind_aws_source" { + source = "overmindtech/aws-source/overmind" + + name = "production" +} +``` + +## Inputs + +| Name | Description | Type | Default | Required | +| --- | --- | --- | --- | --- | +| `name` | Descriptive name for the source in Overmind | `string` | n/a | yes | +| `regions` | AWS regions to discover (defaults to all non-opt-in regions) | `list(string)` | All 17 standard regions | no | +| `role_name` | Name for the IAM role created in this account | `string` | `"overmind-read-only"` | no | +| `tags` | Additional tags to apply to IAM resources | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +| --- | --- | +| `role_arn` | ARN of the created IAM role | +| `source_id` | UUID of the Overmind source | +| `external_id` | AWS STS external ID used in the trust policy | + +## Multi-Account Example + +Use AWS provider aliases to onboard several accounts at once: + +```hcl +provider "overmind" {} + +provider "aws" { + alias = "production" + region = "us-east-1" + assume_role { role_arn = "arn:aws:iam::111111111111:role/terraform" } +} + +provider "aws" { + alias = "staging" + region = "eu-west-1" + assume_role { role_arn = "arn:aws:iam::222222222222:role/terraform" } +} + +module "overmind_production" { + source = "overmindtech/aws-source/overmind" + name = "production" + + providers = { + aws = aws.production + overmind = overmind + } +} + +module "overmind_staging" { + source = "overmindtech/aws-source/overmind" + name = "staging" + regions = ["eu-west-1"] + + providers = { + aws = aws.staging + overmind = overmind + } +} +``` + +## Importing Existing Sources + +If you already created an Overmind AWS source through the UI and want to manage it +with Terraform, you can import it using the source UUID (visible on the source +details page in [Settings > Sources](https://app.overmind.tech/settings/sources)): + +```shell +terraform import module.overmind_aws_source.overmind_aws_source.this +``` + +After importing, run `terraform plan` to verify the state matches your +configuration. Terraform will show any drift between the imported resource and +your HCL. + +## Authentication + +The Overmind provider accepts an API key via the `api_key` attribute or the +`OVERMIND_API_KEY` environment variable. The attribute takes precedence. The key +must have `sources:write` scope. + +```hcl +provider "overmind" { + api_key = var.overmind_api_key +} +``` + +The AWS provider must have permissions to create IAM roles and policies in the +target account. + +## Requirements + +| Name | Version | +| --- | --- | +| terraform | >= 1.5.0 | +| aws | >= 6.0 | +| overmind | >= 0.1.0 | diff --git a/aws-source/module/terraform/examples/multi-account/main.tf b/aws-source/module/terraform/examples/multi-account/main.tf new file mode 100644 index 00000000..af9269f4 --- /dev/null +++ b/aws-source/module/terraform/examples/multi-account/main.tf @@ -0,0 +1,48 @@ +provider "overmind" {} + +provider "aws" { + alias = "production" + region = "us-east-1" + + assume_role { + role_arn = "arn:aws:iam::111111111111:role/terraform" + } +} + +provider "aws" { + alias = "staging" + region = "eu-west-1" + + assume_role { + role_arn = "arn:aws:iam::222222222222:role/terraform" + } +} + +module "overmind_production" { + source = "overmindtech/aws-source/overmind" + name = "production" + + providers = { + aws = aws.production + overmind = overmind + } +} + +module "overmind_staging" { + source = "overmindtech/aws-source/overmind" + name = "staging" + regions = ["eu-west-1"] + + providers = { + aws = aws.staging + overmind = overmind + } +} + +output "production_role_arn" { + value = module.overmind_production.role_arn +} + +output "staging_role_arn" { + value = module.overmind_staging.role_arn +} diff --git a/aws-source/module/terraform/examples/single-account/main.tf b/aws-source/module/terraform/examples/single-account/main.tf new file mode 100644 index 00000000..8043364c --- /dev/null +++ b/aws-source/module/terraform/examples/single-account/main.tf @@ -0,0 +1,19 @@ +provider "overmind" {} + +provider "aws" { + region = "us-east-1" +} + +module "overmind_aws_source" { + source = "overmindtech/aws-source/overmind" + + name = "production" +} + +output "role_arn" { + value = module.overmind_aws_source.role_arn +} + +output "source_id" { + value = module.overmind_aws_source.source_id +} diff --git a/aws-source/module/terraform/main.tf b/aws-source/module/terraform/main.tf new file mode 100644 index 00000000..871bcda5 --- /dev/null +++ b/aws-source/module/terraform/main.tf @@ -0,0 +1,95 @@ +data "overmind_aws_external_id" "this" {} + +resource "aws_iam_role" "overmind" { + name = var.role_name + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { AWS = "arn:aws:iam::${var.overmind_aws_account_id}:root" } + Action = "sts:AssumeRole" + Condition = { + StringEquals = { + "sts:ExternalId" = data.overmind_aws_external_id.this.external_id + } + } + }, + { + Effect = "Allow" + Principal = { AWS = "arn:aws:iam::${var.overmind_aws_account_id}:root" } + Action = "sts:TagSession" + }, + ] + }) + + tags = merge(var.tags, { + "overmind.version" = "2026-02-17" + }) +} + +resource "aws_iam_role_policy" "overmind" { + name = "OvmReadOnly" + role = aws_iam_role.overmind.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "apigateway:Get*", + "autoscaling:Describe*", + "cloudfront:Get*", + "cloudfront:List*", + "cloudwatch:Describe*", + "cloudwatch:GetMetricData", + "cloudwatch:ListTagsForResource", + "directconnect:Describe*", + "dynamodb:Describe*", + "dynamodb:List*", + "ec2:Describe*", + "ecs:Describe*", + "ecs:List*", + "eks:Describe*", + "eks:List*", + "elasticfilesystem:Describe*", + "elasticloadbalancing:Describe*", + "iam:Get*", + "iam:List*", + "kms:Describe*", + "kms:Get*", + "kms:List*", + "lambda:Get*", + "lambda:List*", + "network-firewall:Describe*", + "network-firewall:List*", + "networkmanager:Describe*", + "networkmanager:Get*", + "networkmanager:List*", + "rds:Describe*", + "rds:ListTagsForResource", + "route53:Get*", + "route53:List*", + "s3:GetBucket*", + "s3:ListAllMyBuckets", + "sns:Get*", + "sns:List*", + "sqs:Get*", + "sqs:List*", + "ssm:Describe*", + "ssm:Get*", + "ssm:ListTagsForResource", + ] + Resource = "*" + }, + ] + }) +} + +resource "overmind_aws_source" "this" { + name = var.name + aws_role_arn = aws_iam_role.overmind.arn + aws_regions = var.regions +} diff --git a/aws-source/module/terraform/outputs.tf b/aws-source/module/terraform/outputs.tf new file mode 100644 index 00000000..1521b43d --- /dev/null +++ b/aws-source/module/terraform/outputs.tf @@ -0,0 +1,14 @@ +output "role_arn" { + description = "ARN of the created IAM role." + value = aws_iam_role.overmind.arn +} + +output "source_id" { + description = "UUID of the Overmind source." + value = overmind_aws_source.this.id +} + +output "external_id" { + description = "AWS STS external ID used in the trust policy." + value = data.overmind_aws_external_id.this.external_id +} diff --git a/aws-source/module/terraform/variables.tf b/aws-source/module/terraform/variables.tf new file mode 100644 index 00000000..e002c8d7 --- /dev/null +++ b/aws-source/module/terraform/variables.tf @@ -0,0 +1,46 @@ +variable "name" { + type = string + description = "Descriptive name for the source in Overmind." +} + +variable "regions" { + type = list(string) + default = [ + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ap-south-1", + "ap-northeast-1", + "ap-northeast-2", + "ap-northeast-3", + "ap-southeast-1", + "ap-southeast-2", + "ca-central-1", + "eu-central-1", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-north-1", + "sa-east-1", + ] + description = "AWS regions to discover. Defaults to all non-opt-in regions." +} + +variable "role_name" { + type = string + default = "overmind-read-only" + description = "Name for the IAM role created in this account." +} + +variable "tags" { + type = map(string) + default = {} + description = "Additional tags to apply to IAM resources." +} + +variable "overmind_aws_account_id" { + type = string + default = "942836531449" + description = "Internal override for the Overmind AWS account that runs source pods. Do not change this unless you are an Overmind engineer deploying to a non-production environment. All customers should use the default." +} diff --git a/aws-source/module/terraform/versions.tf b/aws-source/module/terraform/versions.tf new file mode 100644 index 00000000..7c2e5efa --- /dev/null +++ b/aws-source/module/terraform/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + overmind = { + source = "overmindtech/overmind" + version = ">= 0.1.0" + } + } +} diff --git a/aws-source/proc/proc.go b/aws-source/proc/proc.go index c049e7c4..6f0b646c 100644 --- a/aws-source/proc/proc.go +++ b/aws-source/proc/proc.go @@ -41,8 +41,8 @@ import ( stscredsv2 "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -435,6 +435,10 @@ func InitializeAwsSourceAdapters(ctx context.Context, e *discovery.Engine, confi adapters.NewEC2PlacementGroupAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2ReservedInstanceAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2RouteTableAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), + adapters.NewEC2TransitGatewayRouteTableAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), + adapters.NewEC2TransitGatewayRouteTableAssociationAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), + adapters.NewEC2TransitGatewayRouteTablePropagationAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), + adapters.NewEC2TransitGatewayRouteAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2SecurityGroupRuleAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2SecurityGroupAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2SnapshotAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), diff --git a/aws-source/proc/proc_test.go b/aws-source/proc/proc_test.go index 630f050c..69c1f7d3 100644 --- a/aws-source/proc/proc_test.go +++ b/aws-source/proc/proc_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/aws/smithy-go" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/cmd/auth_client.go b/cmd/auth_client.go index 27e2fddd..3666f44d 100644 --- a/cmd/auth_client.go +++ b/cmd/auth_client.go @@ -7,10 +7,10 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" ) diff --git a/cmd/auth_client_test.go b/cmd/auth_client_test.go index 8a18e238..ec4516fc 100644 --- a/cmd/auth_client_test.go +++ b/cmd/auth_client_test.go @@ -14,8 +14,8 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/tracing" ) // testProxyServer is a simple HTTP proxy server for testing diff --git a/cmd/bookmarks_create_bookmark.go b/cmd/bookmarks_create_bookmark.go index 4a2cf333..1e89d038 100644 --- a/cmd/bookmarks_create_bookmark.go +++ b/cmd/bookmarks_create_bookmark.go @@ -8,7 +8,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/bookmarks_get_affected_bookmarks.go b/cmd/bookmarks_get_affected_bookmarks.go index 8b69df2e..e6dc62ef 100644 --- a/cmd/bookmarks_get_affected_bookmarks.go +++ b/cmd/bookmarks_get_affected_bookmarks.go @@ -5,7 +5,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/bookmarks_get_bookmark.go b/cmd/bookmarks_get_bookmark.go index 142cd33c..46a0fe5d 100644 --- a/cmd/bookmarks_get_bookmark.go +++ b/cmd/bookmarks_get_bookmark.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_end_change.go b/cmd/changes_end_change.go index 9d5ca4df..a01cf151 100644 --- a/cmd/changes_end_change.go +++ b/cmd/changes_end_change.go @@ -4,7 +4,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -26,7 +26,11 @@ func EndChange(cmd *cobra.Command, args []string) error { return err } - changeUuid, err := getChangeUUIDAndCheckStatus(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_HAPPENING, viper.GetString("ticket-link"), true) + // Resolve the change UUID without checking status. The server-side + // EndChangeSimple handles status validation atomically and queues end-change + // behind start-change if needed, avoiding the TOCTOU race where status + // transitions between client-side checks. + changeUuid, err := getChangeUUID(ctx, oi, viper.GetString("ticket-link")) if err != nil { return loggedError{ err: err, @@ -36,8 +40,9 @@ func EndChange(cmd *cobra.Command, args []string) error { lf := log.Fields{"uuid": changeUuid.String()} + // Call the simple RPC (enqueues a background job and returns immediately) client := AuthenticatedChangesClient(ctx, oi) - stream, err := client.EndChange(ctx, &connect.Request[sdp.EndChangeRequest]{ + resp, err := client.EndChangeSimple(ctx, &connect.Request[sdp.EndChangeRequest]{ Msg: &sdp.EndChangeRequest{ ChangeUUID: changeUuid[:], }, @@ -49,29 +54,50 @@ func EndChange(cmd *cobra.Command, args []string) error { message: "failed to end change", } } - log.WithContext(ctx).WithFields(lf).Info("processing") - lastLog := time.Now().Add(-1 * time.Minute) - for stream.Receive() { - msg := stream.Msg() - // print progress every 2 seconds - if time.Now().After(lastLog.Add(2 * time.Second)) { + + queuedAfterStart := resp.Msg.GetQueuedAfterStart() + waitForSnapshot := viper.GetBool("wait-for-snapshot") + if waitForSnapshot { + // Poll until change status is DONE + log.WithContext(ctx).WithFields(lf).Info("waiting for snapshot to complete") + for { + changeResp, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], + }, + }) + if err != nil { + return loggedError{ + err: err, + fields: lf, + message: "failed to get change status", + } + } + if changeResp.Msg.GetChange().GetMetadata().GetStatus() == sdp.ChangeStatus_CHANGE_STATUS_DONE { + break + } log.WithContext(ctx).WithFields(lf).WithFields(log.Fields{ - "state": msg.GetState(), - "items": msg.GetNumItems(), - "edges": msg.GetNumEdges(), - }).Info("progress") - lastLog = time.Now() + "status": changeResp.Msg.GetChange().GetMetadata().GetStatus().String(), + }).Info("waiting for snapshot") + time.Sleep(3 * time.Second) + + // check if the context is cancelled + if ctx.Err() != nil { + return loggedError{ + err: ctx.Err(), + fields: lf, + message: "context cancelled", + } + } } - } - if stream.Err() != nil { - return loggedError{ - err: stream.Err(), - fields: lf, - message: "failed to process end change", + log.WithContext(ctx).WithFields(lf).Info("finished change") + } else { + if queuedAfterStart { + log.WithContext(ctx).WithFields(lf).Info("change end queued (will run after start-change completes)") + } else { + log.WithContext(ctx).WithFields(lf).Info("change end initiated (processing in background)") } } - - log.WithContext(ctx).WithFields(lf).Info("finished change") return nil } @@ -79,4 +105,6 @@ func init() { changesCmd.AddCommand(endChangeCmd) addChangeUuidFlags(endChangeCmd) + + endChangeCmd.PersistentFlags().Bool("wait-for-snapshot", false, "Wait for the snapshot to complete before returning. Defaults to false.") } diff --git a/cmd/changes_get_change.go b/cmd/changes_get_change.go index 4237e4c6..7bf1a03d 100644 --- a/cmd/changes_get_change.go +++ b/cmd/changes_get_change.go @@ -8,7 +8,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -72,47 +72,43 @@ func GetChange(cmd *cobra.Command, args []string) error { } client := AuthenticatedChangesClient(ctx, oi) - var timeLine *sdp.GetChangeTimelineV2Response fetch: for { - rawTimeLine, timelineErr := client.GetChangeTimelineV2(ctx, &connect.Request[sdp.GetChangeTimelineV2Request]{ - Msg: &sdp.GetChangeTimelineV2Request{ - ChangeUUID: changeUuid[:], + changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], }, }) - if timelineErr != nil || rawTimeLine.Msg == nil { + if err != nil || changeRes.Msg == nil || changeRes.Msg.GetChange() == nil { return loggedError{ - err: timelineErr, + err: err, fields: lf, - message: "failed to get timeline", + message: "failed to get change", } } - timeLine = rawTimeLine.Msg - for _, entry := range timeLine.GetEntries() { - // ENG-1993: This is temporary to still track the auto tagging entry in the timeline. this is to prevent the cli from hanging - if entry.GetName() == sdp.ChangeTimelineEntryV2IDAutoTagging.Name && entry.GetStatus() == sdp.ChangeTimelineEntryStatus_DONE { - break fetch + ch := changeRes.Msg.GetChange() + md := ch.GetMetadata() + if md == nil || md.GetChangeAnalysisStatus() == nil { + return loggedError{ + err: fmt.Errorf("change metadata or change analysis status is nil"), + fields: lf, + message: "failed to get change", } } - // display the running entry - runningEntry, contentDescription, status, err := sdp.TimelineFindInProgressEntry(timeLine.GetEntries()) - if err != nil { + status := md.GetChangeAnalysisStatus().GetStatus() + switch status { + case sdp.ChangeAnalysisStatus_STATUS_DONE, sdp.ChangeAnalysisStatus_STATUS_SKIPPED: + break fetch + case sdp.ChangeAnalysisStatus_STATUS_ERROR: return loggedError{ - err: err, + err: fmt.Errorf("change analysis completed with error status"), fields: lf, - message: "failed to find running entry", + message: "change analysis failed", } + case sdp.ChangeAnalysisStatus_STATUS_UNSPECIFIED, sdp.ChangeAnalysisStatus_STATUS_INPROGRESS: + log.WithContext(ctx).WithFields(lf).WithField("status", status.String()).Info("Waiting for change analysis to complete") } - // find the running timeline entry - log.WithContext(ctx).WithFields(log.Fields{ - "status": status.String(), - "running": runningEntry, - "content": contentDescription, - }).Info("Waiting for change analysis to complete") - // retry time.Sleep(3 * time.Second) - - // check if the context is cancelled if ctx.Err() != nil { return loggedError{ err: ctx.Err(), diff --git a/cmd/changes_get_change_test.go b/cmd/changes_get_change_test.go index defabcef..55353e56 100644 --- a/cmd/changes_get_change_test.go +++ b/cmd/changes_get_change_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestValidateChangeStatus(t *testing.T) { diff --git a/cmd/changes_get_signals.go b/cmd/changes_get_signals.go index 52d7420c..03bf9ff3 100644 --- a/cmd/changes_get_signals.go +++ b/cmd/changes_get_signals.go @@ -6,7 +6,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -55,47 +55,43 @@ func GetSignals(cmd *cobra.Command, args []string) error { } client := AuthenticatedChangesClient(ctx, oi) - var timeLine *sdp.GetChangeTimelineV2Response fetch: for { - rawTimeLine, timelineErr := client.GetChangeTimelineV2(ctx, &connect.Request[sdp.GetChangeTimelineV2Request]{ - Msg: &sdp.GetChangeTimelineV2Request{ - ChangeUUID: changeUuid[:], + changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], }, }) - if timelineErr != nil || rawTimeLine.Msg == nil { + if err != nil || changeRes.Msg == nil || changeRes.Msg.GetChange() == nil { return loggedError{ - err: timelineErr, + err: err, fields: lf, - message: "failed to get timeline", + message: "failed to get change", } } - timeLine = rawTimeLine.Msg - for _, entry := range timeLine.GetEntries() { - // ENG-1993: This is temporary to still track the auto tagging entry in the timeline. this is to prevent the cli from hanging - if entry.GetName() == sdp.ChangeTimelineEntryV2IDAutoTagging.Name && entry.GetStatus() == sdp.ChangeTimelineEntryStatus_DONE { - break fetch + ch := changeRes.Msg.GetChange() + md := ch.GetMetadata() + if md == nil || md.GetChangeAnalysisStatus() == nil { + return loggedError{ + err: fmt.Errorf("change metadata or change analysis status is nil"), + fields: lf, + message: "failed to get change", } } - // display the running entry - runningEntry, contentDescription, status, err := sdp.TimelineFindInProgressEntry(timeLine.GetEntries()) - if err != nil { + status := md.GetChangeAnalysisStatus().GetStatus() + switch status { + case sdp.ChangeAnalysisStatus_STATUS_DONE, sdp.ChangeAnalysisStatus_STATUS_SKIPPED: + break fetch + case sdp.ChangeAnalysisStatus_STATUS_ERROR: return loggedError{ - err: err, + err: fmt.Errorf("change analysis completed with error status"), fields: lf, - message: "failed to find running entry", + message: "change analysis failed", } + case sdp.ChangeAnalysisStatus_STATUS_UNSPECIFIED, sdp.ChangeAnalysisStatus_STATUS_INPROGRESS: + log.WithContext(ctx).WithFields(lf).WithField("status", status.String()).Info("Waiting for change analysis to complete") } - // find the running timeline entry - log.WithContext(ctx).WithFields(log.Fields{ - "status": status.String(), - "running": runningEntry, - "content": contentDescription, - }).Info("Waiting for change analysis to complete") - // retry time.Sleep(3 * time.Second) - - // check if the context is cancelled if ctx.Err() != nil { return loggedError{ err: ctx.Err(), diff --git a/cmd/changes_list_changes.go b/cmd/changes_list_changes.go index 073ff8bc..7809e340 100644 --- a/cmd/changes_list_changes.go +++ b/cmd/changes_list_changes.go @@ -8,7 +8,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_start_change.go b/cmd/changes_start_change.go index 7d915871..5d757aef 100644 --- a/cmd/changes_start_change.go +++ b/cmd/changes_start_change.go @@ -1,11 +1,11 @@ package cmd import ( - "regexp" + "fmt" "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -43,56 +43,45 @@ func StartChange(cmd *cobra.Command, args []string) error { "ticket-link": viper.GetString("ticket-link"), } - // poll the timeline for the Calculated Blast Radius to be complete + // wait for change analysis to complete (poll GetChange by change_analysis_status) client := AuthenticatedChangesClient(ctx, oi) fetch: for { - rawTimeLine, timelineErr := client.GetChangeTimelineV2(ctx, &connect.Request[sdp.GetChangeTimelineV2Request]{ - Msg: &sdp.GetChangeTimelineV2Request{ - ChangeUUID: changeUuid[:], + changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], }, }) - if timelineErr != nil || rawTimeLine.Msg == nil { + if err != nil || changeRes.Msg == nil || changeRes.Msg.GetChange() == nil { return loggedError{ - err: timelineErr, + err: err, fields: lf, - message: "failed to get timeline", + message: "failed to get change", } } - timeLine := rawTimeLine.Msg - // Use a case-insensitive regex to match any entry containing "blast radius" - blastRadiusRegex := regexp.MustCompile(`(?i)blast\s+radius`) - for _, entry := range timeLine.GetEntries() { - if blastRadiusRegex.MatchString(entry.GetName()) { - if entry.GetStatus() == sdp.ChangeTimelineEntryStatus_DONE { - break fetch - } - if entry.GetStatus() == sdp.ChangeTimelineEntryStatus_ERROR { - // the api server will retry the blast radius calculation, so lets wait for the retry - log.WithContext(ctx).WithFields(lf).Warn("Blast radius calculation failed, waiting for retry") - break - } + ch := changeRes.Msg.GetChange() + md := ch.GetMetadata() + if md == nil || md.GetChangeAnalysisStatus() == nil { + return loggedError{ + err: fmt.Errorf("change metadata or change analysis status is nil"), + fields: lf, + message: "failed to get change", } } - // display the running entry - runningEntry, contentDescription, status, err := sdp.TimelineFindInProgressEntry(timeLine.GetEntries()) - if err != nil { + status := md.GetChangeAnalysisStatus().GetStatus() + switch status { + case sdp.ChangeAnalysisStatus_STATUS_DONE, sdp.ChangeAnalysisStatus_STATUS_SKIPPED: + break fetch + case sdp.ChangeAnalysisStatus_STATUS_ERROR: return loggedError{ - err: err, + err: fmt.Errorf("change analysis completed with error status"), fields: lf, - message: "failed to find running entry", + message: "change analysis failed", } + case sdp.ChangeAnalysisStatus_STATUS_UNSPECIFIED, sdp.ChangeAnalysisStatus_STATUS_INPROGRESS: + log.WithContext(ctx).WithFields(lf).WithField("status", status.String()).Info("Waiting for change analysis to complete") } - // log progress while waiting for blast radius calculation - log.WithContext(ctx).WithFields(log.Fields{ - "status": status.String(), - "running": runningEntry, - "content": contentDescription, - }).Info("Waiting for blast radius to be calculated") - // retry time.Sleep(3 * time.Second) - - // check if the context is cancelled if ctx.Err() != nil { return loggedError{ err: ctx.Err(), @@ -102,7 +91,8 @@ fetch: } } - stream, err := client.StartChange(ctx, &connect.Request[sdp.StartChangeRequest]{ + // Call the simple RPC (enqueues a background job and returns immediately) + _, err = client.StartChangeSimple(ctx, &connect.Request[sdp.StartChangeRequest]{ Msg: &sdp.StartChangeRequest{ ChangeUUID: changeUuid[:], }, @@ -114,29 +104,50 @@ fetch: message: "failed to start change", } } - log.WithContext(ctx).WithFields(lf).Info("processing") - lastLog := time.Now().Add(-1 * time.Minute) - for stream.Receive() { - msg := stream.Msg() - // print progress every 2 seconds - if time.Now().After(lastLog.Add(2 * time.Second)) { + + waitForSnapshot := viper.GetBool("wait-for-snapshot") + if waitForSnapshot { + // Poll until change status has moved on + log.WithContext(ctx).WithFields(lf).Info("waiting for snapshot to complete") + for { + changeResp, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], + }, + }) + if err != nil { + return loggedError{ + err: err, + fields: lf, + message: "failed to get change status", + } + } + status := changeResp.Msg.GetChange().GetMetadata().GetStatus() + // Accept HAPPENING, or DONE: if an end-change was queued during + // start-change, the worker kicks it off atomically and it may complete before + // the next poll, advancing status to DONE. We must not poll indefinitely. + if status == sdp.ChangeStatus_CHANGE_STATUS_HAPPENING || + status == sdp.ChangeStatus_CHANGE_STATUS_DONE { + break + } log.WithContext(ctx).WithFields(lf).WithFields(log.Fields{ - "state": msg.GetState(), - "items": msg.GetNumItems(), - "edges": msg.GetNumEdges(), - }).Info("progress") - lastLog = time.Now() - } - } - if stream.Err() != nil { - return loggedError{ - err: stream.Err(), - fields: lf, - message: "failed to process start change", + "status": status.String(), + }).Info("waiting for snapshot") + time.Sleep(3 * time.Second) + + // check if the context is cancelled + if ctx.Err() != nil { + return loggedError{ + err: ctx.Err(), + fields: lf, + message: "context cancelled", + } + } } + log.WithContext(ctx).WithFields(lf).Info("started change") + } else { + log.WithContext(ctx).WithFields(lf).Info("change start initiated (processing in background)") } - - log.WithContext(ctx).WithFields(lf).Info("started change") return nil } @@ -144,4 +155,6 @@ func init() { changesCmd.AddCommand(startChangeCmd) addChangeUuidFlags(startChangeCmd) + + startChangeCmd.PersistentFlags().Bool("wait-for-snapshot", false, "Wait for the snapshot to complete before returning. Defaults to false.") } diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index 1af78f20..e1eb5035 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -11,8 +11,9 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" + "github.com/overmindtech/cli/knowledge" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -287,6 +288,9 @@ func SubmitPlan(cmd *cobra.Command, args []string) error { routineChangesConfigOverride = signalConfigOverride.RoutineChangesConfig } + // Discover and convert knowledge files + sdpKnowledge := knowledge.DiscoverAndConvert(ctx, ".overmind/knowledge/") + _, err = client.StartChangeAnalysis(ctx, &connect.Request[sdp.StartChangeAnalysisRequest]{ Msg: &sdp.StartChangeAnalysisRequest{ ChangeUUID: changeUUID[:], @@ -294,6 +298,7 @@ func SubmitPlan(cmd *cobra.Command, args []string) error { BlastRadiusConfigOverride: blastRadiusConfigOverride, RoutineChangesConfigOverride: routineChangesConfigOverride, GithubOrganisationProfileOverride: githubOrganisationProfileOverride, + Knowledge: sdpKnowledge, }, }) if err != nil { @@ -305,7 +310,7 @@ func SubmitPlan(cmd *cobra.Command, args []string) error { } app, _ = strings.CutSuffix(app, "/") - changeUrl := fmt.Sprintf("%v/changes/%v/blast-radius", app, changeUUID) + changeUrl := fmt.Sprintf("%v/changes/%v", app, changeUUID) log.WithContext(ctx).WithFields(lf).WithField("change-url", changeUrl).Info("Change ready") fmt.Println(changeUrl) diff --git a/cmd/changes_submit_signal.go b/cmd/changes_submit_signal.go index a218eae6..7e18a3a4 100644 --- a/cmd/changes_submit_signal.go +++ b/cmd/changes_submit_signal.go @@ -5,7 +5,7 @@ import ( "fmt" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/explore.go b/cmd/explore.go index ed2e3550..20df5072 100644 --- a/cmd/explore.go +++ b/cmd/explore.go @@ -18,12 +18,13 @@ import ( "github.com/overmindtech/pterm" "github.com/overmindtech/cli/aws-source/proc" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" azureproc "github.com/overmindtech/cli/sources/azure/proc" gcpproc "github.com/overmindtech/cli/sources/gcp/proc" + snapshotadapters "github.com/overmindtech/cli/sources/snapshot/adapters" stdlibSource "github.com/overmindtech/cli/stdlib-source/adapters" - "github.com/overmindtech/cli/tracing" "github.com/pkg/browser" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" @@ -43,6 +44,8 @@ The CLI automatically discovers and uses: - GCP providers from your Terraform configuration (google and google-beta) - Falls back to default cloud provider credentials if no Terraform providers are found +Set SNAPSHOT_SOURCE to a snapshot file path or URL to run only the snapshot source (no cloud sources will be started). Useful for local testing with fixed data. + For GCP, ensure you have appropriate permissions (roles/browser or equivalent) to access project metadata.`, PreRun: PreRunSetup, @@ -75,6 +78,47 @@ func StartLocalSources(ctx context.Context, oi sdp.OvermindInstance, token *oaut return func() {}, fmt.Errorf("failed to get hostname: %w", err) } + // If SNAPSHOT_SOURCE is set, run ONLY the snapshot source -- skip all live sources. + // Snapshot mode replays pre-recorded data, so cloud providers are unnecessary. + if snapshotSourcePath := os.Getenv("SNAPSHOT_SOURCE"); snapshotSourcePath != "" { + snapshotSpinner, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Starting snapshot source engine (snapshot-only mode)") + + ec := discovery.EngineConfig{ + EngineType: "cli-snapshot", + Version: fmt.Sprintf("cli-%v", tracing.Version()), + SourceName: fmt.Sprintf("snapshot-source-%v", hostname), + SourceUUID: uuid.New(), + App: oi.ApiUrl.Host, + ApiKey: token.AccessToken, + NATSOptions: &natsOpts, + MaxParallelExecutions: 2_000, + HeartbeatOptions: heartbeatOptions(oi, token), + } + snapshotEngine, err := discovery.NewEngine(&ec) + if err != nil { + snapshotSpinner.Fail(fmt.Sprintf("Failed to create snapshot source engine: %v", err)) + return func() {}, fmt.Errorf("failed to create snapshot source engine: %w", err) + } + err = snapshotadapters.InitializeAdapters(ctx, snapshotEngine, snapshotSourcePath) + if err != nil { + snapshotSpinner.Fail(fmt.Sprintf("Failed to initialize snapshot source adapters: %v", err)) + return func() {}, fmt.Errorf("failed to initialize snapshot source adapters: %w", err) + } + err = snapshotEngine.Start(ctx) + if err != nil { + snapshotSpinner.Fail(fmt.Sprintf("Failed to start snapshot source engine: %v", err)) + return func() {}, fmt.Errorf("failed to start snapshot source engine: %w", err) + } + snapshotEngine.StartSendingHeartbeats(ctx) + snapshotSpinner.Success("Snapshot source engine started (snapshot-only mode)") + + return func() { + if err := snapshotEngine.Stop(); err != nil { + log.WithError(err).Error("failed to stop snapshot engine") + } + }, nil + } + p := pool.NewWithResults[[]*discovery.Engine]().WithErrors() // find all the terraform files diff --git a/cmd/flags.go b/cmd/flags.go index 7f6318ed..5a4f3b2d 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/spf13/cobra" "github.com/spf13/viper" ) diff --git a/cmd/flags_test.go b/cmd/flags_test.go index 14e7e14b..5521757a 100644 --- a/cmd/flags_test.go +++ b/cmd/flags_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/spf13/viper" ) diff --git a/cmd/integrations_tfc.go b/cmd/integrations_tfc.go index 7d2b4513..7305805f 100644 --- a/cmd/integrations_tfc.go +++ b/cmd/integrations_tfc.go @@ -5,7 +5,7 @@ import ( "fmt" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) diff --git a/cmd/invites_crud.go b/cmd/invites_crud.go index 40963e47..f5839f70 100644 --- a/cmd/invites_crud.go +++ b/cmd/invites_crud.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/jedib0t/go-pretty/v6/table" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/pterm.go b/cmd/pterm.go index abb1eec7..3818ec37 100644 --- a/cmd/pterm.go +++ b/cmd/pterm.go @@ -15,11 +15,11 @@ import ( "connectrpc.com/connect" "github.com/overmindtech/pterm" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" "github.com/spf13/cobra" diff --git a/cmd/request.go b/cmd/request.go index 2926239e..7596d4b8 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -4,8 +4,8 @@ import ( "context" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpws" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpws" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/request_load.go b/cmd/request_load.go index 2bbd779c..2ff6a1a1 100644 --- a/cmd/request_load.go +++ b/cmd/request_load.go @@ -6,9 +6,9 @@ import ( "os" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpws" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpws" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/request_query.go b/cmd/request_query.go index af84d075..6c083703 100644 --- a/cmd/request_query.go +++ b/cmd/request_query.go @@ -7,9 +7,9 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpws" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpws" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -171,8 +171,7 @@ func CreateQuery() (*sdp.Query, error) { Deadline: timestamppb.New(time.Now().Add(10 * time.Hour)), UUID: u[:], RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: viper.GetUint32("link-depth"), - FollowOnlyBlastPropagation: viper.GetBool("blast-radius"), + LinkDepth: viper.GetUint32("link-depth"), }, IgnoreCache: viper.GetBool("ignore-cache"), }, nil @@ -196,5 +195,4 @@ func init() { requestQueryCmd.PersistentFlags().String("snapshot-description", "none", "The snapshot description of the query results") requestQueryCmd.PersistentFlags().Uint32("link-depth", 0, "How deeply to link") - requestQueryCmd.PersistentFlags().Bool("blast-radius", false, "Whether to query using blast radius, note that if using this option, link-depth should be set to > 0") } diff --git a/cmd/root.go b/cmd/root.go index a262b11b..512f4f95 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,9 +22,9 @@ import ( josejwt "github.com/go-jose/go-jose/v4/jwt" "github.com/google/uuid" "github.com/overmindtech/pterm" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" "github.com/pkg/browser" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -247,6 +247,64 @@ func getChangeUUIDAndCheckStatus(ctx context.Context, oi sdp.OvermindInstance, e return changeUUID, nil } +// getChangeUUID resolves a change UUID from --uuid, --change, or --ticket-link without +// checking the change status. Use this when the server-side RPC handles status validation +// (e.g. EndChangeSimple already validates status atomically and has queuing logic). +func getChangeUUID(ctx context.Context, oi sdp.OvermindInstance, ticketLink string) (uuid.UUID, error) { + uuidString := viper.GetString("uuid") + changeUrlString := viper.GetString("change") + + // If no arguments are specified then return an error + if uuidString == "" && changeUrlString == "" && ticketLink == "" { + return uuid.Nil, errors.New("no change specified; use one of --change, --ticket-link or --uuid") + } + + // Check UUID first if more than one is set + if uuidString != "" { + changeUUID, err := uuid.Parse(uuidString) + if err != nil { + return uuid.Nil, fmt.Errorf("invalid --uuid value '%v', error: %w", uuidString, err) + } + trace.SpanFromContext(ctx).SetAttributes( + attribute.String("ovm.change.uuid", changeUUID.String()), + ) + return changeUUID, nil + } + + // Then check for a change URL + if changeUrlString != "" { + uuidFromChangeURL, err := parseChangeUrl(changeUrlString) + if err != nil { + return uuidFromChangeURL, err + } + trace.SpanFromContext(ctx).SetAttributes( + attribute.String("ovm.change.uuid", uuidFromChangeURL.String()), + ) + return uuidFromChangeURL, nil + } + + // Finally look up by ticket link (single attempt, no status check) + client := AuthenticatedChangesClient(ctx, oi) + change, err := client.GetChangeByTicketLink(ctx, &connect.Request[sdp.GetChangeByTicketLinkRequest]{ + Msg: &sdp.GetChangeByTicketLinkRequest{ + TicketLink: ticketLink, + }, + }) + if err != nil { + return uuid.Nil, fmt.Errorf("error looking up change with ticket link %v: %w", ticketLink, err) + } + + uuidPtr := change.Msg.GetChange().GetMetadata().GetUUIDParsed() + if uuidPtr == nil { + return uuid.Nil, fmt.Errorf("change found with ticket link %v but has no UUID", ticketLink) + } + + trace.SpanFromContext(ctx).SetAttributes( + attribute.String("ovm.change.uuid", uuidPtr.String()), + ) + return *uuidPtr, nil +} + // getChangeByTicketLinkWithRetry performs the GetChangeByTicketLink API call with retry logic, // retrying both on error and when the status does not match the expected status. // NB api-server will only return the latest change with this ticket link. diff --git a/cmd/root_test.go b/cmd/root_test.go index 4f926537..bac3a178 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/auth" + "github.com/overmindtech/cli/go/auth" "golang.org/x/oauth2" ) diff --git a/cmd/snapshots_create.go b/cmd/snapshots_create.go index 3e965547..22253f86 100644 --- a/cmd/snapshots_create.go +++ b/cmd/snapshots_create.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpws" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpws" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/snapshots_get_snapshot.go b/cmd/snapshots_get_snapshot.go index ce90080b..10914241 100644 --- a/cmd/snapshots_get_snapshot.go +++ b/cmd/snapshots_get_snapshot.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index 0617c2bf..43f72216 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -11,7 +11,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" "github.com/overmindtech/pterm" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index fae484b7..8a3d343e 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -16,8 +16,9 @@ import ( "github.com/google/uuid" "github.com/muesli/reflow/wordwrap" "github.com/overmindtech/pterm" + "github.com/overmindtech/cli/knowledge" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -314,10 +315,14 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI uploadPlannedChange, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Uploading planned changes") log.WithField("change", changeUuid).Debug("Uploading planned changes") + // Discover and convert knowledge files + sdpKnowledge := knowledge.DiscoverAndConvert(ctx, ".overmind/knowledge/") + _, err = client.StartChangeAnalysis(ctx, &connect.Request[sdp.StartChangeAnalysisRequest]{ Msg: &sdp.StartChangeAnalysisRequest{ ChangeUUID: changeUuid[:], ChangingItems: mappingResponse.GetItemDiffs(), + Knowledge: sdpKnowledge, }, }) if err != nil { @@ -327,77 +332,67 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI uploadPlannedChange.Success("Uploaded planned changes: Done") changeUrl := *oi.FrontendUrl - changeUrl.Path = fmt.Sprintf("%v/changes/%v/blast-radius", changeUrl.Path, changeUuid) + changeUrl.Path = fmt.Sprintf("%v/changes/%v", changeUrl.Path, changeUuid) log.WithField("change-url", changeUrl.String()).Info("Change ready") /////////////////////////////////////////////////////////////////// - // wait for change analysis to complete + // wait for change analysis to complete (poll GetChange by change_analysis_status) /////////////////////////////////////////////////////////////////// changeAnalysisSpinner, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Change Analysis") - var timeLine *sdp.GetChangeTimelineV2Response - milestoneSpinners := []*pterm.SpinnerPrinter{} retryLoop: for { - rawTimeLine, timelineErr := client.GetChangeTimelineV2(ctx, &connect.Request[sdp.GetChangeTimelineV2Request]{ - Msg: &sdp.GetChangeTimelineV2Request{ - ChangeUUID: changeUuid[:], + changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], }, }) - if timelineErr != nil || rawTimeLine.Msg == nil { - changeAnalysisSpinner.Fail(fmt.Sprintf("Change Analysis failed to get timeline: %v", timelineErr)) - return nil + if err != nil { + changeAnalysisSpinner.Fail(fmt.Sprintf("Change Analysis failed to get change: %v", err)) + return fmt.Errorf("failed to get change during change analysis: %w", err) } - timeLine = rawTimeLine.Msg - - // display the status for the timeline entries - for i, entry := range timeLine.GetEntries() { - // populate the spinner list on the first run - if i <= len(milestoneSpinners) { - milestoneSpinners = append(milestoneSpinners, pterm.DefaultSpinner. - WithWriter(multi.NewWriter()). - WithIndentation(IndentSymbol()). - WithText(entry.GetName())) - } - // render the spinner for this entry - switch entry.GetStatus() { - case sdp.ChangeTimelineEntryStatus_PENDING: - continue - case sdp.ChangeTimelineEntryStatus_IN_PROGRESS: - if !milestoneSpinners[i].IsActive { - milestoneSpinners[i], _ = milestoneSpinners[i].Start() - } - case sdp.ChangeTimelineEntryStatus_ERROR: - milestoneSpinners[i].Fail() - case sdp.ChangeTimelineEntryStatus_DONE: - milestoneSpinners[i].Success() - case sdp.ChangeTimelineEntryStatus_UNSPECIFIED: - // do nothing - default: - milestoneSpinners[i].Fail(fmt.Sprintf("Unknown status: %v", entry.GetStatus())) - } - - // ENG-1993: This is temporary to still track the auto tagging entry in the timeline. this is to prevent the cli from hanging - // check if change analysis is done - if entry.GetName() == sdp.ChangeTimelineEntryV2IDAutoTagging.Name && entry.GetStatus() == sdp.ChangeTimelineEntryStatus_DONE { - changeAnalysisSpinner.Success() - break retryLoop - } + if changeRes.Msg == nil || changeRes.Msg.GetChange() == nil { + changeAnalysisSpinner.Fail("Change Analysis failed: received empty change response") + return fmt.Errorf("change analysis failed: received empty change response") + } + ch := changeRes.Msg.GetChange() + md := ch.GetMetadata() + if md == nil || md.GetChangeAnalysisStatus() == nil { + changeAnalysisSpinner.Fail("Change Analysis failed: change metadata or analysis status missing") + return fmt.Errorf("change analysis failed: change metadata or change analysis status is nil") + } + status := md.GetChangeAnalysisStatus().GetStatus() + switch status { + case sdp.ChangeAnalysisStatus_STATUS_DONE, sdp.ChangeAnalysisStatus_STATUS_SKIPPED: + changeAnalysisSpinner.Success() + break retryLoop + case sdp.ChangeAnalysisStatus_STATUS_ERROR: + changeAnalysisSpinner.Fail("Change analysis failed") + return fmt.Errorf("change analysis completed with error status") + case sdp.ChangeAnalysisStatus_STATUS_UNSPECIFIED, sdp.ChangeAnalysisStatus_STATUS_INPROGRESS: + // keep polling } - // retry time.Sleep(3 * time.Second) - } - var calculateRiskStep *sdp.ChangeTimelineEntryV2 - for _, entry := range timeLine.GetEntries() { - if entry.GetName() == sdp.ChangeTimelineEntryV2IDCalculatedRisks.Name { - calculateRiskStep = entry - break + if ctx.Err() != nil { + changeAnalysisSpinner.Fail("Cancelled") + return ctx.Err() } } - if calculateRiskStep == nil || calculateRiskStep.GetCalculatedRisks() == nil { - return fmt.Errorf("Failed to get calculated risks") + risksRes, err := client.GetChangeRisks(ctx, &connect.Request[sdp.GetChangeRisksRequest]{ + Msg: &sdp.GetChangeRisksRequest{ + UUID: changeUuid[:], + }, + }) + if err != nil { + return fmt.Errorf("failed to get calculated risks: %w", err) + } + if risksRes.Msg == nil { + return fmt.Errorf("failed to get calculated risks: response message was nil") + } + if risksRes.Msg.GetChangeRiskMetadata() == nil { + return fmt.Errorf("failed to get calculated risks: change risk metadata was nil") } - calculatedRisks := calculateRiskStep.GetCalculatedRisks().GetRisks() + calculatedRisks := risksRes.Msg.GetChangeRiskMetadata().GetRisks() // Submit milestone for tracing if cmdSpan != nil { cmdSpan.AddEvent("Change Analysis finished", trace.WithAttributes( diff --git a/docs.overmind.tech/docs/sources/_category_.json b/docs.overmind.tech/docs/sources/_category_.json new file mode 100644 index 00000000..974a8cf6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Infrastructure Sources", + "position": 3, + "collapsed": true, + "link": null +} diff --git a/docs.overmind.tech/docs/sources/aws/Types/apigateway-domain-name.md b/docs.overmind.tech/docs/sources/aws/Types/apigateway-domain-name.md new file mode 100644 index 00000000..bb16518e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/apigateway-domain-name.md @@ -0,0 +1,16 @@ +--- +title: API Gateway Domain Name +sidebar_label: apigateway-domain-name +--- + +An AWS API Gateway Domain Name represents a custom DNS name (e.g. `api.example.com`) that you attach to one or more stages of a REST, HTTP or WebSocket API. By creating this resource you can present a branded, user-friendly endpoint instead of the default `*.execute-api..amazonaws.com` host, configure an ACM or imported TLS certificate, choose an edge-optimised or regional endpoint, enable mutual TLS and define API mappings. Further information can be found in the official documentation: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-create-custom-domain-name.html + +**Terrafrom Mappings:** + +- `aws_api_gateway_domain_name.domain_name` + +## Supported Methods + +- `GET`: Get a Domain Name by domain-name +- `LIST`: List Domain Names +- `SEARCH`: Search Domain Names by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/apigateway-resource.md b/docs.overmind.tech/docs/sources/aws/Types/apigateway-resource.md new file mode 100644 index 00000000..2816caa9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/apigateway-resource.md @@ -0,0 +1,17 @@ +--- +title: API Gateway +sidebar_label: apigateway-resource +--- + +An **API Gateway Resource** represents a single path segment within an Amazon API Gateway REST API. Each resource forms part of the hierarchical URL structure of your API and can have HTTP methods (such as GET, POST, DELETE) attached to it, along with integrations, authorisers and request/response models. Correctly mapping these resources is critical because mis-configured paths can expose unintended back-ends or shadow existing routes. Overmind pulls every API Gateway Resource into its graph so you can understand how proposed changes will affect downstream services before you deploy them. +For further details, refer to the official AWS documentation: https://docs.aws.amazon.com/apigateway/latest/api/API_Resource.html + +**Terrafrom Mappings:** + +- `aws_api_gateway_resource.id` + +## Supported Methods + +- `GET`: Get a Resource by rest-api-id/resource-id +- ~~`LIST`~~ +- `SEARCH`: Search Resources by REST API ID diff --git a/docs.overmind.tech/docs/sources/aws/Types/apigateway-rest-api.md b/docs.overmind.tech/docs/sources/aws/Types/apigateway-rest-api.md new file mode 100644 index 00000000..83569578 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/apigateway-rest-api.md @@ -0,0 +1,27 @@ +--- +title: REST API +sidebar_label: apigateway-rest-api +--- + +AWS API Gateway REST APIs allow you to build, deploy and manage REST-style interfaces that front your application logic, Lambda functions or other AWS services. A REST API in API Gateway represents the top-level container for all stages, resources, methods, authorisers and deployments that make up your service. Once created, the API can be exposed publicly or kept private behind a VPC endpoint, throttled, monitored and versioned across stages. +For full details, refer to the official AWS documentation: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html + +**Terrafrom Mappings:** + +- `aws_api_gateway_rest_api.id` + +## Supported Methods + +- `GET`: Get a REST API by ID +- `LIST`: List all REST APIs +- `SEARCH`: Search for REST APIs by their name + +## Possible Links + +### [`ec2-vpc-endpoint`](/sources/aws/Types/ec2-vpc-endpoint) + +If the REST API is configured as a private API, it is exposed inside a VPC through an Interface VPC Endpoint. Overmind links the `apigateway-rest-api` resource to the corresponding `ec2-vpc-endpoint` to show which endpoint clients inside the VPC must use to reach the API and to surface any network-level risks (such as missing security-group rules). + +### [`apigateway-resource`](/sources/aws/Types/apigateway-resource) + +An API Gateway REST API is composed of one or more resources, each representing a path segment (for example `/users` or `/orders/{orderId}`). Overmind links the parent `apigateway-rest-api` to each individual `apigateway-resource` so you can trace how a request traverses the API hierarchy and identify unprotected or redundant paths. diff --git a/docs.overmind.tech/docs/sources/aws/Types/autoscaling-auto-scaling-group.md b/docs.overmind.tech/docs/sources/aws/Types/autoscaling-auto-scaling-group.md new file mode 100644 index 00000000..03b3db07 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/autoscaling-auto-scaling-group.md @@ -0,0 +1,39 @@ +--- +title: Autoscaling Group +sidebar_label: autoscaling-auto-scaling-group +--- + +An AWS Autoscaling Group (ASG) is a logical collection of Amazon EC2 instances that are treated as a single scalable resource. It automatically adjusts the number of running instances to maintain a desired capacity, respond to demand spikes, enforce health‐based replacement, and support rolling updates. Configuration parameters such as minimum, maximum and desired instance counts, scaling policies, health checks and lifecycle hooks are all defined at the group level. +Further information is available in the official AWS documentation: https://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html + +**Terrafrom Mappings:** + +- `aws_autoscaling_group.arn` + +## Supported Methods + +- `GET`: Get an Autoscaling Group by name +- `LIST`: List Autoscaling Groups +- `SEARCH`: Search for Autoscaling Groups by ARN + +## Possible Links + +### [`ec2-launch-template`](/sources/aws/Types/ec2-launch-template) + +An ASG normally references a launch template that describes how each EC2 instance should be configured (AMI, instance type, security groups, IAM instance profile, user data, etc.). Therefore the ASG is linked to its associated `ec2-launch-template`. + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +ASGs can be attached to one or more ALB/NLB target groups so that their member instances are automatically registered and deregistered as they scale. The link shows which `elbv2-target-group`(s) an ASG feeds. + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +The running EC2 instances that currently belong to an ASG are directly related to it. Overmind surfaces this connection so you can see which `ec2-instance` objects are under the control of a specific ASG. + +### [`iam-role`](/sources/aws/Types/iam-role) + +Autoscaling uses an AWS service-linked role (typically `AWSServiceRoleForAutoScaling`) to perform scaling and health check actions on your behalf. Additionally, the launch template referenced by the ASG may specify an instance profile containing an IAM role for the launched instances. Both relationships are captured via the `iam-role` link. + +### [`ec2-placement-group`](/sources/aws/Types/ec2-placement-group) + +If the ASG’s launch template specifies a placement group, any instances it launches will be placed accordingly for improved networking performance or spread. The link reveals the `ec2-placement-group` associated with the ASG. diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-cache-policy.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-cache-policy.md new file mode 100644 index 00000000..892c7bac --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-cache-policy.md @@ -0,0 +1,16 @@ +--- +title: CloudFront Cache Policy +sidebar_label: cloudfront-cache-policy +--- + +An AWS CloudFront Cache Policy specifies the rules that dictate how CloudFront caches HTTP responses at edge locations. It determines which headers, cookies and query-string parameters are included in the cache key, how long objects remain in the cache (TTL values), and whether to compress the response before it is served to viewers. By creating and attaching custom cache policies to distributions or behaviours, you can fine-tune cache efficiency, control origin load, and optimise performance for different types of content. For a full description of the resource and its attributes, refer to the [AWS documentation](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CachePolicy.html). + +**Terrafrom Mappings:** + +- `aws_cloudfront_cache_policy.id` + +## Supported Methods + +- `GET`: Get a CloudFront Cache Policy +- `LIST`: List CloudFront Cache Policies +- `SEARCH`: Search CloudFront Cache Policies by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-continuous-deployment-policy.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-continuous-deployment-policy.md new file mode 100644 index 00000000..49f4679c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-continuous-deployment-policy.md @@ -0,0 +1,19 @@ +--- +title: CloudFront Continuous Deployment Policy +sidebar_label: cloudfront-continuous-deployment-policy +--- + +A CloudFront Continuous Deployment Policy is an Amazon CloudFront configuration object that allows you to shift viewer traffic between two CloudFront distributions (normally a _staging_ and a _production_ distribution) in a controlled, progressive way. By defining percentage-based traffic splits or header-based routing rules, you can carry out blue/green or canary releases, test new versions of your application, and roll back instantly if problems occur. +Official documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/continuous-deployment.html + +## Supported Methods + +- `GET`: Get a CloudFront Continuous Deployment Policy by ID +- `LIST`: List CloudFront Continuous Deployment Policies +- `SEARCH`: Search CloudFront Continuous Deployment Policies by ARN + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +DNS records (usually CNAME or ALIAS/ANAME) that point end-user domains to the target CloudFront distributions determine which viewers are subject to a continuous deployment policy. When a policy is enabled, those DNS entries still resolve to the same CloudFront hostnames, but the policy decides how the resulting requests are routed internally between the staging and production distributions. Overmind therefore links the policy to related DNS resources so you can trace which public hostnames—and consequently which users—are affected by a particular traffic-splitting setup. diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-distribution.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-distribution.md new file mode 100644 index 00000000..8222aaa1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-distribution.md @@ -0,0 +1,58 @@ +--- +title: CloudFront Distribution +sidebar_label: cloudfront-distribution +--- + +Amazon CloudFront Distributions are globally-replicated configurations that tell the CloudFront CDN how to cache and deliver your content to end-users. Each distribution defines one or more origins, cache behaviours, security settings and optional edge-compute integrations. See the official AWS documentation for a full description: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_distribution.arn` + +## Supported Methods + +- `GET`: Get a distribution by ID +- `LIST`: List all distributions +- `SEARCH`: Search distributions by ARN + +## Possible Links + +### [`cloudfront-key-group`](/sources/aws/Types/cloudfront-key-group) + +A distribution can reference one or more Key Groups in its `TrustedKeyGroups` configuration to validate signed URLs or signed cookies. If a Key Group ID appears in the distribution’s config, Overmind links the two. + +### [`cloudfront-continuous-deployment-policy`](/sources/aws/Types/cloudfront-continuous-deployment-policy) + +Distributions may have an attached Continuous Deployment Policy (`ContinuousDeploymentPolicyId`) that allows blue/green traffic shifting. Overmind links the distribution to that policy. + +### [`cloudfront-cache-policy`](/sources/aws/Types/cloudfront-cache-policy) + +Every cache behaviour in a distribution can specify a `CachePolicyId`. Overmind links the distribution to any Cache Policies it relies on. + +### [`cloudfront-function`](/sources/aws/Types/cloudfront-function) + +Viewer request / response CloudFront Functions can be associated with behaviours in the distribution. Those references create links between the distribution and the function resources. + +### [`cloudfront-origin-request-policy`](/sources/aws/Types/cloudfront-origin-request-policy) + +Behaviours can also specify an `OriginRequestPolicyId` that controls which headers, cookies and query strings are sent to the origin. Overmind links distributions to the referenced Origin Request Policies. + +### [`cloudfront-realtime-log-config`](/sources/aws/Types/cloudfront-realtime-log-config) + +If real-time logging is enabled, the distribution contains one or more `RealtimeLogConfigArn` values. Overmind uses those to link the distribution to its real-time log configuration. + +### [`cloudfront-response-headers-policy`](/sources/aws/Types/cloudfront-response-headers-policy) + +Behaviours may include a `ResponseHeadersPolicyId` that injects security or custom headers. Overmind links the distribution to the associated Response Headers Policies. + +### [`dns`](/sources/stdlib/Types/dns) + +Public access to a distribution is normally via the CloudFront domain name or an alias/CNAME such as `www.example.com`. When a DNS record (e.g., Route 53 ALIAS) targets the distribution’s domain, Overmind links the DNS record to the distribution. + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +Lambda@Edge functions (standard Lambda functions replicated to edge locations) can be attached to behaviours for request or response processing. These associations create links between the distribution and the Lambda functions. + +### [`s3-bucket`](/sources/aws/Types/s3-bucket) + +An S3 bucket is commonly used as an origin. When the distribution’s origin points at an S3 bucket domain or ARN, Overmind links the distribution to that bucket. diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-function.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-function.md new file mode 100644 index 00000000..dc43f440 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-function.md @@ -0,0 +1,16 @@ +--- +title: CloudFront Function +sidebar_label: cloudfront-function +--- + +Amazon CloudFront Functions let you run lightweight JavaScript code at CloudFront edge locations, enabling real-time manipulation of HTTP requests and responses without the latency of invoking AWS Lambda. Typical use-cases include URL rewrites, header manipulation, access control and A/B testing, all executed in under one millisecond at every edge. For more detail see the official AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_function.name` + +## Supported Methods + +- `GET`: Get a CloudFront Function by name +- `LIST`: List CloudFront Functions +- `SEARCH`: Search CloudFront Functions by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-key-group.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-key-group.md new file mode 100644 index 00000000..f2647ffb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-key-group.md @@ -0,0 +1,17 @@ +--- +title: CloudFront Key Group +sidebar_label: cloudfront-key-group +--- + +A CloudFront Key Group is an Amazon CloudFront configuration object that aggregates several public keys under a single identifier. CloudFront uses the keys in the group to verify the signatures on signed URLs, signed cookies, or JSON Web Tokens that you employ to control access to private content. By attaching a key group to a distribution or cache behaviour you can centrally manage which public keys are trusted; adding or removing a key from the group immediately changes who can generate valid signatures without the need to touch individual distributions. +For more information, refer to the AWS documentation on Key Groups: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html#PrivateContent-KeyGroups + +**Terrafrom Mappings:** + +- `aws_cloudfront_key_group.id` + +## Supported Methods + +- `GET`: Get a CloudFront Key Group by ID +- `LIST`: List CloudFront Key Groups +- `SEARCH`: Search CloudFront Key Groups by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-access-control.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-access-control.md new file mode 100644 index 00000000..42454fe8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-access-control.md @@ -0,0 +1,17 @@ +--- +title: Cloudfront Origin Access Control +sidebar_label: cloudfront-origin-access-control +--- + +Amazon CloudFront Origin Access Control (OAC) is a security feature that allows you to restrict access to the origin of a CloudFront distribution, ensuring that all requests are authenticated and authorised by CloudFront before reaching your S3 bucket, Application Load Balancer, or custom origin. OAC is the modern replacement for Origin Access Identities (OAI) and supports both SigV4‐signed requests and IAM authentication, giving you more granular control over how CloudFront communicates with your back-end resources. By configuring an OAC you prevent direct exposure of your origin on the public internet, helping to mitigate data-exfiltration and origin-based attacks. +For further information see the official AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-origin.html#concept-origin-access-control + +**Terrafrom Mappings:** + +- `aws_cloudfront_origin_access_control.id` + +## Supported Methods + +- `GET`: Get Origin Access Control by ID +- `LIST`: List Origin Access Controls +- `SEARCH`: Origin Access Control by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-request-policy.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-request-policy.md new file mode 100644 index 00000000..2b59261c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-request-policy.md @@ -0,0 +1,17 @@ +--- +title: CloudFront Origin Request Policy +sidebar_label: cloudfront-origin-request-policy +--- + +A CloudFront Origin Request Policy defines which HTTP headers, cookies and query-string parameters Amazon CloudFront passes from the edge to your origin. By attaching a policy to a cache behaviour you can standardise the information that reaches your origin, independent of any caching decisions. Policies are reusable across multiple distributions, making configuration simpler and less error-prone. +For further details refer to the [AWS documentation](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_OriginRequestPolicy.html). + +**Terrafrom Mappings:** + +- `aws_cloudfront_origin_request_policy.id` + +## Supported Methods + +- `GET`: Get Origin Request Policy by ID +- `LIST`: List Origin Request Policies +- `SEARCH`: Origin Request Policy by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-realtime-log-config.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-realtime-log-config.md new file mode 100644 index 00000000..5202bc2c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-realtime-log-config.md @@ -0,0 +1,17 @@ +--- +title: CloudFront Realtime Log Config +sidebar_label: cloudfront-realtime-log-config +--- + +Amazon CloudFront Realtime Log Configs define the structure of the near-real-time log data that CloudFront can stream to a destination such as Kinesis Data Streams. A Realtime Log Config specifies which data fields are captured, the sampling rate, and the endpoint to which the records are delivered. This enables teams to observe viewer requests, latency, cache behaviour and other metrics with sub-second visibility, allowing faster troubleshooting and performance tuning. +For a detailed description of the service and its capabilities, refer to the official AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/real-time-logs.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_realtime_log_config.arn` + +## Supported Methods + +- `GET`: Get Realtime Log Config by Name +- `LIST`: List Realtime Log Configs +- `SEARCH`: Search Realtime Log Configs by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-response-headers-policy.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-response-headers-policy.md new file mode 100644 index 00000000..74c25ae6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-response-headers-policy.md @@ -0,0 +1,17 @@ +--- +title: CloudFront Response Headers Policy +sidebar_label: cloudfront-response-headers-policy +--- + +A CloudFront Response Headers Policy is an AWS configuration object that specifies the HTTP response headers that Amazon CloudFront adds to, removes from, or overrides on the responses it returns to viewers. By defining a policy you can, for example, enforce security-related headers (such as `Strict-Transport-Security` or `Content-Security-Policy`), apply custom cache-control directives, or expose additional headers to browsers for client-side logic. Once created, a response headers policy can be associated with one or more CloudFront distributions, allowing consistent header behaviour across multiple delivery configurations. +For full details see the AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/response-headers-policies.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_response_headers_policy.id` + +## Supported Methods + +- `GET`: Get Response Headers Policy by ID +- `LIST`: List Response Headers Policies +- `SEARCH`: Search Response Headers Policy by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-streaming-distribution.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-streaming-distribution.md new file mode 100644 index 00000000..91412fd6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-streaming-distribution.md @@ -0,0 +1,23 @@ +--- +title: CloudFront Streaming Distribution +sidebar_label: cloudfront-streaming-distribution +--- + +An Amazon CloudFront Streaming Distribution is a special type of CloudFront distribution optimised for on-demand media streaming (historically using the RTMP protocol) and for serving video content over HTTP/S from an origin such as Amazon S3 or an on-premises media server. It automatically places edge cache nodes close to viewers, reducing latency and bandwidth costs while providing scalability, encryption and access control options. For full details see the official AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-streaming.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_distribution.arn` +- `aws_cloudfront_distribution.id` + +## Supported Methods + +- `GET`: Get a Streaming Distribution by ID +- `LIST`: List Streaming Distributions +- `SEARCH`: Search Streaming Distributions by ARN + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +Each CloudFront Streaming Distribution is reachable via a unique domain name that ends in `cloudfront.net`, and may also be associated with custom CNAMEs. These domain names appear in DNS records that overmind can discover and connect to the distribution resource. diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudwatch-alarm.md b/docs.overmind.tech/docs/sources/aws/Types/cloudwatch-alarm.md new file mode 100644 index 00000000..eb3b85cb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudwatch-alarm.md @@ -0,0 +1,17 @@ +--- +title: CloudWatch Alarm +sidebar_label: cloudwatch-alarm +--- + +An Amazon CloudWatch Alarm watches a single CloudWatch metric (or a maths expression based on one or more metrics) and performs one or more actions when the metric breaches a threshold for a specified number of evaluation periods. Typical actions include sending an SNS notification, invoking an Auto Scaling policy or stopping, terminating, rebooting or recovering an EC2 instance. Alarms are therefore often a critical part of operational resilience and cost-control strategies, and mis-configuration can lead to missed incidents or unwanted automated actions. +Official documentation: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html + +**Terraform Mappings:** + +- `aws_cloudwatch_metric_alarm.alarm_name` + +## Supported Methods + +- `GET`: Get an alarm by name +- `LIST`: List all alarms +- `SEARCH`: Search for alarms. This accepts JSON in the format of `cloudwatch.DescribeAlarmsForMetricInput` diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-connection.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-connection.md new file mode 100644 index 00000000..0d08e744 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-connection.md @@ -0,0 +1,30 @@ +--- +title: Connection +sidebar_label: directconnect-connection +--- + +An AWS Direct Connect Connection represents a single dedicated network circuit between your on-premises environment (or colocation facility) and an AWS Direct Connect location. By provisioning a connection you obtain a physical 1 Gbps, 10 Gbps or 100 Gbps port on an AWS router, through which you can create one or more virtual interfaces to reach AWS services or your VPCs. A connection is the fundamental building-block for achieving consistent, low-latency private connectivity into AWS, bypassing the public Internet and allowing you to commit to specific bandwidth and service-level requirements. See the official AWS documentation for further details: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithConnections.html + +**Terrafrom Mappings:** + +- `aws_dx_connection.id` + +## Supported Methods + +- `GET`: Get a connection by ID +- `LIST`: List all connections +- `SEARCH`: Search connection by ARN + +## Possible Links + +### [`directconnect-lag`](/sources/aws/Types/directconnect-lag) + +A Link Aggregation Group (LAG) can aggregate one or more individual connections into a single managed logical interface. A connection may belong to a LAG, and conversely a LAG lists each underlying connection that forms part of the group. + +### [`directconnect-location`](/sources/aws/Types/directconnect-location) + +Every connection is terminated at a specific Direct Connect location (e.g. an Equinix or Digital Realty data centre). The connection resource references its chosen location to indicate where the physical port is installed. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +Virtual interfaces (public, private or transit) are configured on top of a connection to carry customer traffic. Each virtual interface is associated with exactly one connection (or LAG), while a single connection can host multiple virtual interfaces for different routing purposes. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-customer-metadata.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-customer-metadata.md new file mode 100644 index 00000000..1e33f6b8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-customer-metadata.md @@ -0,0 +1,13 @@ +--- +title: Customer Metadata +sidebar_label: directconnect-customer-metadata +--- + +Customer Metadata represents the customer agreement that is on file for your AWS account in relation to AWS Direct Connect. The record contains information such as the name and Amazon Resource Name (ARN) of the agreement, its current revision and status, and the Region in which the agreement applies. Being able to inspect this resource lets you confirm that the correct contractual terms have been accepted before you attempt to create or modify Direct Connect connections, helping you avoid deployment failures that stem from missing or outdated agreements. +For further details see the AWS API documentation: https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DescribeCustomerMetadata.html + +## Supported Methods + +- `GET`: Get a customer agreement by name +- `LIST`: List all customer agreements +- `SEARCH`: Search customer agreements by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association-proposal.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association-proposal.md new file mode 100644 index 00000000..54d9add6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association-proposal.md @@ -0,0 +1,24 @@ +--- +title: Direct Connect Gateway Association Proposal +sidebar_label: directconnect-direct-connect-gateway-association-proposal +--- + +An AWS Direct Connect Gateway Association Proposal represents a cross-account request to attach a Virtual Private Gateway (VGW) or Transit Gateway (TGW) to an existing Direct Connect Gateway (DXGW). +The proposal is created by the owner of the VGW/TGW and must be accepted by the DXGW owner before the association is established. It contains details such as allowed prefixes and the identifiers of the gateways involved, providing both parties with a clear record of what will change once the proposal is accepted. +For more information, see the official AWS API documentation: https://docs.aws.amazon.com/directconnect/latest/APIReference/API_CreateDirectConnectGatewayAssociationProposal.html + +**Terrafrom Mappings:** + +- `aws_dx_gateway_association_proposal.id` + +## Supported Methods + +- `GET`: Get a Direct Connect Gateway Association Proposal by ID +- `LIST`: List all Direct Connect Gateway Association Proposals +- `SEARCH`: Search Direct Connect Gateway Association Proposals by ARN + +## Possible Links + +### [`directconnect-direct-connect-gateway-association`](/sources/aws/Types/directconnect-direct-connect-gateway-association) + +A proposal, once accepted, becomes a Direct Connect Gateway Association. Therefore, every accepted `directconnect-direct-connect-gateway-association-proposal` will have a corresponding `directconnect-direct-connect-gateway-association` resource that represents the live attachment between the DXGW and the VGW/TGW. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association.md new file mode 100644 index 00000000..2f7f64bd --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association.md @@ -0,0 +1,23 @@ +--- +title: Direct Connect Gateway Association +sidebar_label: directconnect-direct-connect-gateway-association +--- + +A Direct Connect Gateway Association represents the attachment of a virtual private gateway (VGW) or a transit gateway (TGW) to an AWS Direct Connect gateway. Once associated, the on-premises network that is connected through an AWS Direct Connect dedicated or hosted connection can reach the VPCs behind the VGW/TGW, even if they are in different AWS Regions. +For more detail, see the AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/direct-connect-gateways-intro.html#direct-connect-gateway-associations + +**Terraform Mappings:** + +- `aws_dx_gateway_association.id` + +## Supported Methods + +- `GET`: Get a direct connect gateway association by direct connect gateway ID and virtual gateway ID +- ~~`LIST`~~ +- `SEARCH`: Search direct connect gateway associations by direct connect gateway ID + +## Possible Links + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +A Direct Connect Gateway Association is a child resource of a Direct Connect Gateway, so every association is linked to the Direct Connect Gateway to which the VGW/TGW is attached. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-attachment.md new file mode 100644 index 00000000..86f6233b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-attachment.md @@ -0,0 +1,23 @@ +--- +title: Direct Connect Gateway Attachment +sidebar_label: directconnect-direct-connect-gateway-attachment +--- + +An AWS Direct Connect **gateway attachment** represents the binding between a Direct Connect Gateway and a Virtual Interface (VIF). When the attachment is in the `attached` state, traffic that reaches the VIF can be routed to any VPCs or on-premises networks that are associated with the gateway, even across accounts or Regions. +For a full description of the concept, states, and quotas involved, see the AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/direct-connect-gateways.html#dx-gateway-attachments + +## Supported Methods + +- `GET`: Get a direct connect gateway attachment by DirectConnectGatewayId/VirtualInterfaceId +- ~~`LIST`~~ +- `SEARCH`: Search direct connect gateway attachments for given VirtualInterfaceId + +## Possible Links + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +Each gateway attachment belongs to exactly one Direct Connect Gateway. Overmind links the attachment back to its parent gateway so you can see every VIF that is currently associated with that gateway. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +The attachment is also linked to the Virtual Interface that is being attached. This lets you trace which VIFs are connected to which gateways and, in turn, to the networks that sit behind those gateways. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway.md new file mode 100644 index 00000000..4deb30f5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway.md @@ -0,0 +1,17 @@ +--- +title: Direct Connect Gateway +sidebar_label: directconnect-direct-connect-gateway +--- + +An AWS Direct Connect gateway is a global virtual routing resource that allows you to attach one or more Direct Connect private virtual interfaces to one or more Virtual Private Gateways (VGWs) or Transit Gateways (TGWs) across any AWS Region (with the exception of the AWS China Regions). By decoupling the physical Direct Connect connection from a specific VPC or Region, it simplifies multi-region and multi-account network architectures, provides centralised route control, and reduces the number of BGP sessions that need to be managed. +For a detailed overview, refer to the official AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/direct-connect-gateways.html + +**Terrafrom Mappings:** + +- `aws_dx_gateway.id` + +## Supported Methods + +- `GET`: Get a direct connect gateway by ID +- `LIST`: List all direct connect gateways +- `SEARCH`: Search direct connect gateway by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-hosted-connection.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-hosted-connection.md new file mode 100644 index 00000000..332dadab --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-hosted-connection.md @@ -0,0 +1,31 @@ +--- +title: Hosted Connection +sidebar_label: directconnect-hosted-connection +--- + +A **Hosted Connection** is an AWS Direct Connect circuit that is provisioned for you by an AWS Direct Connect Delivery Partner on their own network infrastructure and then allocated to your AWS account. It provides a dedicated, layer-2 link that terminates at an AWS Direct Connect location and can be used to create virtual interfaces (VIFs) to access AWS services or your VPCs. Unlike dedicated connections, hosted connections are requested from the partner rather than from AWS directly, and their capacity is limited to 50 Mbps, 100 Mbps, 200 Mbps, 300 Mbps, 400 Mbps or 500 Mbps. +See the official AWS documentation for full details: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithConnections.html#HostedConnections + +**Terrafrom Mappings:** + +- `aws_dx_hosted_connection.id` + +## Supported Methods + +- `GET`: Get a Hosted Connection by connection ID +- ~~`LIST`~~ +- `SEARCH`: Search Hosted Connections by Interconnect or LAG ID + +## Possible Links + +### [`directconnect-lag`](/sources/aws/Types/directconnect-lag) + +A hosted connection can be delivered over a Link Aggregation Group (LAG). In this case the LAG is the parent resource that physically contains the hosted connection, so the hosted connection links **to** its associated LAG. + +### [`directconnect-location`](/sources/aws/Types/directconnect-location) + +Every hosted connection terminates at a specific AWS Direct Connect location (for example, a colocation data centre). The hosted connection therefore links **to** the location where its physical port is situated. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +After a hosted connection becomes available you create one or more virtual interfaces on top of it. These virtual interfaces depend on the hosted connection, so they link **from** the hosted connection. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-interconnect.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-interconnect.md new file mode 100644 index 00000000..aed4e26f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-interconnect.md @@ -0,0 +1,27 @@ +--- +title: Interconnect +sidebar_label: directconnect-interconnect +--- + +An AWS Direct Connect **Interconnect** is a high-capacity physical Ethernet link (10 Gbps or 100 Gbps) between an AWS Direct Connect location and the network of an approved network service provider. The provider uses the interconnect to carve out and allocate Hosted Connections or Hosted Virtual Interfaces for individual customer accounts, allowing many end-users to share the same physical infrastructure while maintaining logical separation and security. In Overmind, the **directconnect-interconnect** type lets you surface configuration details (such as bandwidth, location, and operational state) and map its relationships to other Direct Connect resources so you can spot mis-configuration or single-point-of-failure risks before deployment. +For authoritative information see the AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithInterconnects.html + +## Supported Methods + +- `GET`: Get a Interconnect by InterconnectId +- `LIST`: List all Interconnects +- `SEARCH`: Search Interconnects by ARN + +## Possible Links + +### [`directconnect-hosted-connection`](/sources/aws/Types/directconnect-hosted-connection) + +Hosted connections are provisioned on top of an Interconnect. Each hosted connection link points back to the parent Interconnect that physically carries its traffic. + +### [`directconnect-lag`](/sources/aws/Types/directconnect-lag) + +LAGs (Link Aggregation Groups) created on an Interconnect combine multiple physical ports of that Interconnect into a single logical interface, increasing bandwidth and providing redundancy. + +### [`directconnect-location`](/sources/aws/Types/directconnect-location) + +Every Interconnect terminates at a specific Direct Connect location such as an AWS-aligned colocation facility; this link shows where the Interconnect is physically hosted. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-lag.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-lag.md new file mode 100644 index 00000000..e7c62741 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-lag.md @@ -0,0 +1,31 @@ +--- +title: Link Aggregation Group +sidebar_label: directconnect-lag +--- + +An AWS Direct Connect **Link Aggregation Group (LAG)** allows you to combine multiple physical Direct Connect connections into a single, logical interface. Doing so simplifies management, provides higher aggregate bandwidth and offers built-in resiliency: if one underlying connection goes down, traffic is automatically redistributed across the remaining links. Each LAG behaves as a single port on the AWS side while still exposing the individual connections (with their own light-levels and alarms) for troubleshooting. +Official AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/lag.html + +**Terrafrom Mappings:** + +- `aws_dx_lag.id` + +## Supported Methods + +- `GET`: Get a Link Aggregation Group by ID +- `LIST`: List all Link Aggregation Groups +- `SEARCH`: Search Link Aggregation Group by ARN + +## Possible Links + +### [`directconnect-connection`](/sources/aws/Types/directconnect-connection) + +A LAG is essentially a collection of Direct Connect connections. Each linked `directconnect-connection` represents one of the physical ports that has been bundled into the LAG. + +### [`directconnect-hosted-connection`](/sources/aws/Types/directconnect-hosted-connection) + +Hosted connections can also be associated with a LAG. Overmind links these `directconnect-hosted-connection` resources to show which hosted (customer-provisioned) circuits are aggregated under the same LAG. + +### [`directconnect-location`](/sources/aws/Types/directconnect-location) + +Every LAG is created at a specific AWS Direct Connect location (data centre or colocation facility). The `directconnect-location` link identifies the physical site where the LAG’s constituent connections terminate. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-location.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-location.md new file mode 100644 index 00000000..5d5f6c1b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-location.md @@ -0,0 +1,17 @@ +--- +title: Direct Connect Location +sidebar_label: directconnect-location +--- + +An AWS Direct Connect Location represents one of the globally distributed, carrier-neutral data-centre facilities where you can order and terminate an AWS Direct Connect dedicated circuit. Each location has a unique location code that you reference when requesting a connection, viewing available port speeds, generating LOAs, or validating the physical site of an existing circuit. Understanding which locations are available – and the risks or constraints linked to each – helps you design resilient, low-latency connectivity between your on-premises network and AWS. +For full details see the official AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithLocations.html + +**Terrafrom Mappings:** + +- `aws_dx_location.location_code` + +## Supported Methods + +- `GET`: Get a Location by its code +- `LIST`: List all Direct Connect Locations +- `SEARCH`: Search Direct Connect Locations by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-router-configuration.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-router-configuration.md new file mode 100644 index 00000000..cf5c91d4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-router-configuration.md @@ -0,0 +1,23 @@ +--- +title: Router Configuration +sidebar_label: directconnect-router-configuration +--- + +AWS Direct Connect can automatically generate a sample configuration that you can paste into the customer-side router that terminates a private, public or transit virtual interface. The Router Configuration object represents that text file. Because the template is created by AWS specifically for the selected virtual interface it already contains the correct BGP ASN, VLAN, IP addressing and other parameters for the connection, reducing the chance of a mis-configuration. +Full details of the API can be found in the AWS Direct Connect API Reference: https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DescribeRouterConfiguration.html + +**Terrafrom Mappings:** + +- `aws_dx_router_configuration.virtual_interface_id` + +## Supported Methods + +- `GET`: Get a Router Configuration by Virtual Interface ID +- ~~`LIST`~~ +- `SEARCH`: Search Router Configuration by ARN + +## Possible Links + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +A Router Configuration is generated for, and therefore has a **1-to-1** relationship with, a Direct Connect Virtual Interface. The link allows you to navigate from the virtual interface to the exact configuration you should apply to your on-premises router (and vice-versa), making it easier to validate that the interface has been deployed according to the recommended configuration. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-gateway.md new file mode 100644 index 00000000..24a64425 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-gateway.md @@ -0,0 +1,14 @@ +--- +title: Direct Connect Virtual Gateway +sidebar_label: directconnect-virtual-gateway +--- + +A Direct Connect virtual gateway (sometimes called a virtual private gateway, or **VGW**) is the AWS-managed end-point that terminates a private virtual interface and presents it to your Amazon VPC. It provides the control-plane for routing traffic between your on-premises network and one or more VPCs over an AWS Direct Connect link, removing the need to run VPN hardware or BGP sessions inside the VPC itself. By querying this resource, Overmind can show you which VPCs and Direct Connect virtual interfaces are affected, surface any missing or insecure route advertisements, and highlight configuration drift _before_ changes are deployed. + +For more information, refer to the AWS Direct Connect documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/virtual-gateway.html + +## Supported Methods + +- `GET`: Get a virtual gateway by ID +- `LIST`: List all virtual gateways +- `SEARCH`: Search virtual gateways by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-interface.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-interface.md new file mode 100644 index 00000000..4d7222f1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-interface.md @@ -0,0 +1,41 @@ +--- +title: Virtual Interface +sidebar_label: directconnect-virtual-interface +--- + +A Virtual Interface (VIF) is the logical layer that sits on top of an AWS Direct Connect physical connection and provides Layer 3 access into AWS. Three flavours are available—private, public and transit—each supporting different routing destinations and services. A VIF defines the VLAN, BGP peering IPs, Autonomous System Numbers (ASNs), jumbo-frame settings and, optionally, a Direct Connect Gateway association. +Official AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithVirtualInterfaces.html + +**Terrafrom Mappings:** + +- `aws_dx_private_virtual_interface.id` +- `aws_dx_public_virtual_interface.id` +- `aws_dx_transit_virtual_interface.id` + +## Supported Methods + +- `GET`: Get a virtual interface by ID +- `LIST`: List all virtual interfaces +- `SEARCH`: Search virtual interfaces by connection ID + +## Possible Links + +### [`directconnect-connection`](/sources/aws/Types/directconnect-connection) + +Every VIF must be created against a Direct Connect physical connection. The link lets you trace which circuit (location, port speed, AWS account) the virtual interface is riding on. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +Private and transit VIFs can be attached to a Direct Connect Gateway to reach multiple VPCs or on-premises networks. This link shows that association, helping you see the downstream network blast-radius of a VIF change. + +### [`rdap-ip-network`](/sources/stdlib/Types/rdap-ip-network) + +The BGP peer IPs configured on a VIF belong to specific IPv4/IPv6 networks. Linking to RDAP IP network objects allows visibility of route-origin information and public registration data for those peer addresses. + +### [`directconnect-direct-connect-gateway-attachment`](/sources/aws/Types/directconnect-direct-connect-gateway-attachment) + +When a VIF is associated with a Direct Connect Gateway, an attachment resource is created in AWS. This link maps the VIF to its attachment object so you can understand and audit that relationship. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +Some organisations create multiple VIFs on the same physical connection for isolation (e.g., production vs. test). Overmind links sibling VIFs so you can view parallel logical circuits that share the same underlay. diff --git a/docs.overmind.tech/docs/sources/aws/Types/dynamodb-backup.md b/docs.overmind.tech/docs/sources/aws/Types/dynamodb-backup.md new file mode 100644 index 00000000..5b2aa507 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/dynamodb-backup.md @@ -0,0 +1,18 @@ +--- +title: DynamoDB Backup +sidebar_label: dynamodb-backup +--- + +A DynamoDB Backup represents a point-in-time, fully-managed snapshot of an Amazon DynamoDB table, including all of its data and global secondary indexes. Back-ups can be created on demand or retained automatically through continuous point-in-time recovery (PITR). They allow you to restore the table to any state within the retention window, or to clone the data into a new table in the same or another region for testing and disaster-recovery purposes. For further details, see the official AWS documentation: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/BackupRestore.html + +## Supported Methods + +- ~~`GET`~~ +- `LIST`: List all DynamoDB backups +- `SEARCH`: Search for a DynamoDB backup by table name + +## Possible Links + +### [`dynamodb-table`](/sources/aws/Types/dynamodb-table) + +Each backup is intrinsically tied to the table from which it was taken; Overmind therefore links a `dynamodb-backup` item to its source `dynamodb-table` so you can trace data-protection coverage, understand restore scopes, and assess the blast radius of table changes or deletions. diff --git a/docs.overmind.tech/docs/sources/aws/Types/dynamodb-table.md b/docs.overmind.tech/docs/sources/aws/Types/dynamodb-table.md new file mode 100644 index 00000000..85dfe110 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/dynamodb-table.md @@ -0,0 +1,27 @@ +--- +title: DynamoDB Table +sidebar_label: dynamodb-table +--- + +Amazon DynamoDB is AWS’s fully-managed NoSQL database service, providing single-millisecond latency at virtually any scale. A DynamoDB table is the primary container for data, storing items as key–value pairs and supporting features such as on-demand or provisioned capacity, global replication, streams and automatic encryption at rest. +For a full description of table capabilities, limits and API operations, see the official AWS documentation: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Table.html + +**Terrafrom Mappings:** + +- `aws_dynamodb_table.arn` + +## Supported Methods + +- `GET`: Get a DynamoDB table by name +- `LIST`: List all DynamoDB tables +- `SEARCH`: Search for DynamoDB tables by ARN + +## Possible Links + +### [`dynamodb-table`](/sources/aws/Types/dynamodb-table) + +When a table participates in a global table configuration, each regional replica is represented as a separate `dynamodb-table` item. Overmind links these peer replicas so that you can see the full set of regions involved in the same globally replicated table. + +### [`kms-key`](/sources/aws/Types/kms-key) + +If server-side encryption is enabled with a customer-managed KMS key, the table is linked to the `kms-key` that protects its data. This allows you to trace encryption dependencies and assess the impact of key rotation or deletion. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-address.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-address.md new file mode 100644 index 00000000..62cf7c5e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-address.md @@ -0,0 +1,31 @@ +--- +title: EC2 Address +sidebar_label: ec2-address +--- + +An EC2 Address represents an Elastic IP (EIP) in AWS – a static, public IPv4 address that you can allocate to your account and assign to running resources such as EC2 instances or network interfaces. Elastic IPs let you mask the failure of a single instance by rapidly remapping the address to another resource, ensuring minimal disruption to services that rely on a fixed public endpoint. See the official AWS documentation for full details: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html + +**Terrafrom Mappings:** + +- `aws_eip.public_ip` +- `aws_eip_association.public_ip` + +## Supported Methods + +- `GET`: Get an EC2 address by Public IP +- `LIST`: List EC2 addresses +- `SEARCH`: Search for EC2 addresses by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +An Elastic IP can be attached directly to an EC2 instance; this link shows which instance currently holds (or most recently held) the address, allowing you to trace external reachability back to the compute resource. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The Elastic IP is ultimately a routable IPv4 address; this link connects the high-level EIP object to the underlying IP entity so that you can track dependencies and overlap with other networking resources in your estate. + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +When an Elastic IP is associated with an EC2 instance, it is actually bound to one of the instance’s network interfaces (ENIs). This link identifies the specific ENI, enabling deeper analysis of traffic flow, security groups, and subnet placement that pertain to the EIP. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation-fleet.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation-fleet.md new file mode 100644 index 00000000..51b2b13a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation-fleet.md @@ -0,0 +1,19 @@ +--- +title: Capacity Reservation Fleet +sidebar_label: ec2-capacity-reservation-fleet +--- + +A Capacity Reservation Fleet is an Amazon EC2 resource that lets you create and manage a group of Capacity Reservations in a single operation. By specifying instance attributes such as instance types, platforms and Availability Zones, you can ensure that the compute capacity your workload requires will be held for you ahead of time, even during periods of high demand. This is especially useful when you need to guarantee that a heterogeneous mix of instances will be available at launch, for example during large-scale events or disaster-recovery drills. +For more information, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateCapacityReservationFleet.html + +## Supported Methods + +- `GET`: Get a capacity reservation fleet by ID +- `LIST`: List capacity reservation fleets +- `SEARCH`: Search capacity reservation fleets by ARN + +## Possible Links + +### [`ec2-capacity-reservation`](/sources/aws/Types/ec2-capacity-reservation) + +A Capacity Reservation Fleet is essentially an umbrella object that owns one or more individual Capacity Reservations. Each linked `ec2-capacity-reservation` represents a single slice of capacity that was created as part of the fleet’s allocation strategy, and tracking these links lets you understand which reservations belong to which fleet and how capacity is distributed across instance types and Availability Zones. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation.md new file mode 100644 index 00000000..1ef498bd --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation.md @@ -0,0 +1,27 @@ +--- +title: Capacity Reservation +sidebar_label: ec2-capacity-reservation +--- + +An Amazon EC2 Capacity Reservation is an AWS construct that sets aside compute capacity for one or more instance types in a specific Availability Zone, guaranteeing that the reserved capacity is available whenever you need to launch instances. Capacity Reservations can be created individually or as members of a Capacity Reservation Fleet, allowing you to reserve capacity across several instance types and Zones in a single request. This is particularly useful for workloads that must start at short notice, seasonal traffic peaks, or disaster-recovery scenarios. +For a detailed explanation, refer to the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-capacity-reservations.html + +**Terrafrom Mappings:** + +- `aws_ec2_capacity_reservation_fleet.id` + +## Supported Methods + +- `GET`: Get a capacity reservation fleet by ID +- `LIST`: List capacity reservation fleets +- `SEARCH`: Search capacity reservation fleets by ARN + +## Possible Links + +### [`ec2-placement-group`](/sources/aws/Types/ec2-placement-group) + +A Capacity Reservation can be scoped to a placement group. When the `placement_group_arn` (or equivalent Terraform argument) is specified, Overmind links the reservation to that placement group so you can see how the reserved capacity aligns with your low-latency or HPC topology. + +### [`ec2-capacity-reservation-fleet`](/sources/aws/Types/ec2-capacity-reservation-fleet) + +If the reservation was created as part of a Capacity Reservation Fleet, Overmind links it to its parent fleet. This lets you trace individual reservations back to the fleet that manages them and understand how they contribute to the overall pool of reserved capacity across instance types and Availability Zones. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-egress-only-internet-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-egress-only-internet-gateway.md new file mode 100644 index 00000000..a4b31106 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-egress-only-internet-gateway.md @@ -0,0 +1,23 @@ +--- +title: Egress Only Internet Gateway +sidebar_label: ec2-egress-only-internet-gateway +--- + +An Egress Only Internet Gateway (EOIGW) is a horizontally-scaled, highly available AWS VPC component that allows outbound-only IPv6 traffic from your VPC to the internet while preventing unsolicited inbound connections. Unlike a standard Internet Gateway, an EOIGW supports IPv6 traffic exclusively and enforces one-way egress, making it a useful control when you want resources such as application servers to reach external IPv6 services without being directly reachable from the internet. +For detailed information, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway.html + +**Terrafrom Mappings:** + +- `egress_only_internet_gateway.id` + +## Supported Methods + +- `GET`: Get an egress only internet gateway by ID +- `LIST`: List all egress only internet gateways +- `SEARCH`: Search egress only internet gateways by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +An EOIGW is attached to exactly one VPC. Overmind represents this relationship so that you can navigate from a VPC to its associated egress-only internet gateways and understand which networks can initiate outbound IPv6 traffic to the internet. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-iam-instance-profile-association.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-iam-instance-profile-association.md new file mode 100644 index 00000000..9c55da00 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-iam-instance-profile-association.md @@ -0,0 +1,23 @@ +--- +title: IAM Instance Profile Association +sidebar_label: ec2-iam-instance-profile-association +--- + +An IAM Instance Profile Association represents the live binding between an Amazon EC2 instance and an IAM instance profile (which in turn wraps an IAM role). The association determines which IAM permissions the instance receives via its metadata service. Only one profile can be associated with an instance at a time; changing the association effectively swaps the role that the instance assumes. +For further information see the AWS API reference: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_IamInstanceProfileAssociation.html + +## Supported Methods + +- `GET`: Get an IAM Instance Profile Association by ID +- `LIST`: List all IAM Instance Profile Associations +- `SEARCH`: Search IAM Instance Profile Associations by ARN + +## Possible Links + +### [`iam-instance-profile`](/sources/aws/Types/iam-instance-profile) + +The association points to exactly one IAM instance profile, identifying the set of IAM permissions that will be handed to the EC2 instance. + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +Each association belongs to a single EC2 instance, indicating which profile (and hence which role) the instance is currently using. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-image.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-image.md new file mode 100644 index 00000000..1d1ed76d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-image.md @@ -0,0 +1,17 @@ +--- +title: Amazon Machine Image (AMI) +sidebar_label: ec2-image +--- + +An Amazon Machine Image (AMI) is a pre-configured, read-only template that defines the software stack required to launch an Amazon EC2 instance. It typically contains an operating system, application server, and any additional software or configuration needed for your workload. By selecting or creating an AMI you can reproduce identical instances at scale, roll back to known-good states, or share hardened golden images across accounts and Regions. +For a full explanation of AMIs, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html. + +**Terrafrom Mappings:** + +- `aws_ami.id` + +## Supported Methods + +- `GET`: Get an AMI by ID +- `LIST`: List all AMIs +- `SEARCH`: Search AMIs by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-event-window.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-event-window.md new file mode 100644 index 00000000..0b47e4f7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-event-window.md @@ -0,0 +1,19 @@ +--- +title: EC2 Instance Event Window +sidebar_label: ec2-instance-event-window +--- + +An EC2 Instance Event Window is an Amazon EC2 scheduling feature that lets you specify one or more preferred time ranges during which planned AWS maintenance events (for example, a reboot, stop/start or software update) may be applied to your instances. By defining event windows, you retain greater control over when service-initiated interruptions occur, enabling you to align maintenance with your own change-management processes and minimise unplanned impact. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/event-windows.html + +## Supported Methods + +- `GET`: Get an event window by ID +- `LIST`: List all event windows +- `SEARCH`: Search for event windows by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +An event window can be associated with one or more EC2 instances. When a linkage exists, those instances will only receive scheduled maintenance events during the time ranges defined in the referenced EC2 Instance Event Window. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-status.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-status.md new file mode 100644 index 00000000..8b065a05 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-status.md @@ -0,0 +1,14 @@ +--- +title: EC2 Instance Status +sidebar_label: ec2-instance-status +--- + +An EC2 Instance Status record summarises the current health of a running Amazon Elastic Compute Cloud (EC2) instance. AWS performs two types of status checks—system checks (that assess the underlying host and network) and instance checks (that confirm the guest operating system is reachable). Together they indicate whether the instance is able to accept traffic and function as expected. +Overmind ingests these status objects so that you can surface potential availability risks (e.g. persistent instance check failures) before promoting or modifying a deployment. +For a detailed explanation of how AWS generates and interprets these checks, see the [official AWS documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/status-checks.html). + +## Supported Methods + +- `GET`: Get an EC2 instance status by Instance ID +- `LIST`: List all EC2 instance statuses +- `SEARCH`: Search EC2 instance statuses by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-instance.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance.md new file mode 100644 index 00000000..2d969a8f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance.md @@ -0,0 +1,67 @@ +--- +title: EC2 Instance +sidebar_label: ec2-instance +--- + +An Amazon EC2 instance is a resizable virtual server that runs in the AWS cloud and provides the compute layer of most workloads. Instances can be started, stopped, terminated, resized and placed into different networking or storage configurations, allowing you to run applications without purchasing physical hardware. For full details see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Instances.html + +**Terrafrom Mappings:** + +- `aws_instance.id` +- `aws_instance.arn` + +## Supported Methods + +- `GET`: Get an EC2 instance by ID +- `LIST`: List all EC2 instances +- `SEARCH`: Search EC2 instances by ARN + +## Possible Links + +### [`ec2-instance-status`](/sources/aws/Types/ec2-instance-status) + +Represents the current state of the instance (pending, running, stopping, stopped, etc.), health checks, and scheduled events. + +### [`iam-instance-profile`](/sources/aws/Types/iam-instance-profile) + +An instance can be launched with an IAM instance profile, enabling the software running on it to assume a role and gain AWS permissions. + +### [`ec2-capacity-reservation`](/sources/aws/Types/ec2-capacity-reservation) + +If the instance is launched into a specific capacity reservation, that reservation object is linked here to show the source of reserved compute capacity. + +### [`ec2-image`](/sources/aws/Types/ec2-image) + +Every EC2 instance is created from an Amazon Machine Image (AMI). This link points to the AMI used at launch time. + +### [`ec2-key-pair`](/sources/aws/Types/ec2-key-pair) + +For Linux and some Windows instances a key pair is specified for SSH/RDP access; the referenced key pair is linked here. + +### [`ec2-placement-group`](/sources/aws/Types/ec2-placement-group) + +Instances can be placed in a placement group to influence network performance or availability. This link shows that relationship. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Each instance receives one or more private and, optionally, public IP addresses. These addresses are surfaced as separate `ip` resources linked to the instance. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +The instance’s primary network interface is attached to a specific subnet; that subnet is linked to reveal networking context. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The subnet (and thus the instance) resides inside a VPC. Linking the VPC shows the broader network boundary and associated routing. + +### [`dns`](/sources/stdlib/Types/dns) + +Public and private DNS names resolve to the instance’s IP addresses; these DNS records are connected through this link. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +One or more security groups control inbound and outbound traffic to the instance network interfaces. Those groups are linked here. + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +EBS volumes attached to the instance for root and additional block storage are represented and linked by this type. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-internet-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-internet-gateway.md new file mode 100644 index 00000000..209f7d21 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-internet-gateway.md @@ -0,0 +1,23 @@ +--- +title: Internet Gateway +sidebar_label: ec2-internet-gateway +--- + +An Internet Gateway is a highly-available, horizontally-scaled component that provides a Virtual Private Cloud (VPC) with a route to the public Internet. When attached to a VPC and referenced in the route table, it enables resources with public IP addresses—such as EC2 instances, NAT gateways or load balancers—to send and receive traffic to and from the wider Internet. Because it is a managed AWS service, it does not introduce any single point of failure and requires no administration beyond attachment and routing. +For the official AWS documentation, see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html. + +**Terrafrom Mappings:** + +- `aws_internet_gateway.id` + +## Supported Methods + +- `GET`: Get an internet gateway by ID +- `LIST`: List all internet gateways +- `SEARCH`: Search internet gateways by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +An Internet Gateway must be attached to exactly one VPC; this link represents that one-to-one relationship. Through it, Overmind can surface configuration drift (for example, if the gateway is detached) and highlight risks such as missing or overly permissive route-table entries that would expose private resources to the Internet. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-key-pair.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-key-pair.md new file mode 100644 index 00000000..dc2d6700 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-key-pair.md @@ -0,0 +1,17 @@ +--- +title: Key Pair +sidebar_label: ec2-key-pair +--- + +An Amazon EC2 Key Pair is a set of cryptographic keys that enables secure, password-less SSH access to your EC2 instances and other compatible services. The public key is stored in AWS, while the private key is downloaded and managed by you. If the private key is compromised or lost, access to the associated instances is at risk, so tracking key pairs is critical for security posture. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html + +**Terrafrom Mappings:** + +- `aws_key_pair.id` + +## Supported Methods + +- `GET`: Get a key pair by name +- `LIST`: List all key pairs +- `SEARCH`: Search for key pairs by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template-version.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template-version.md new file mode 100644 index 00000000..993c306c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template-version.md @@ -0,0 +1,51 @@ +--- +title: Launch Template Version +sidebar_label: ec2-launch-template-version +--- + +An AWS EC2 Launch Template Version is an immutable snapshot of all the parameters that make up a particular revision of an EC2 launch template – such as AMI ID, instance type, network interfaces, storage, tags and user-data. Each version can be referenced directly when launching instances or by services like Auto Scaling, Spot Fleets and EC2 Fleet, giving you reproducible, auditable instance configuration. +For full details see the AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateVersion.html + +## Supported Methods + +- `GET`: Get a launch template version by `{templateId}.{version}` +- `LIST`: List all launch template versions +- `SEARCH`: Search launch template versions by ARN + +## Possible Links + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +The version can embed zero or more network interface specifications, each of which becomes an `ec2-network-interface` when an instance is launched from the template. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +Within the network interface or placement settings the version may reference a specific subnet ID, tying the launched instance to that `ec2-subnet`. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Security group IDs listed in the template control inbound and outbound traffic for instances started from this version, linking it to the relevant `ec2-security-group` resources. + +### [`ec2-image`](/sources/aws/Types/ec2-image) + +Every launch template version specifies an AMI ID, creating a dependency on the corresponding `ec2-image`. + +### [`ec2-key-pair`](/sources/aws/Types/ec2-key-pair) + +If a key name is supplied, the version references an `ec2-key-pair` used for SSH access to Linux instances or password encryption for Windows instances. + +### [`ec2-snapshot`](/sources/aws/Types/ec2-snapshot) + +EBS block-device mappings in the template can point to snapshot IDs, establishing a relationship with the relevant `ec2-snapshot` objects. + +### [`ec2-capacity-reservation`](/sources/aws/Types/ec2-capacity-reservation) + +The template may include a capacity reservation target, associating the version with a specific `ec2-capacity-reservation`. + +### [`ec2-placement-group`](/sources/aws/Types/ec2-placement-group) + +Placement settings in the version can name a placement group, indicating that instances should launch into the linked `ec2-placement-group`. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Static private or public IP addresses specified in the network interface configuration will be materialised as `ip` resources when the template version is used to launch an instance. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template.md new file mode 100644 index 00000000..81457635 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template.md @@ -0,0 +1,17 @@ +--- +title: Launch Template +sidebar_label: ec2-launch-template +--- + +An EC2 Launch Template is an AWS resource that stores the complete configuration needed to spin up one or more Amazon EC2 instances, including AMI ID, instance type, network settings, user-data scripts, and optional purchasing options such as Spot or On-Demand. By saving these parameters in a versioned template, teams can reproduce environments consistently, roll back to previous configurations, and simplify autoscaling and fleet operations. +Official documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-templates.html + +**Terrafrom Mappings:** + +- `aws_launch_template.id` + +## Supported Methods + +- `GET`: Get a launch template by ID +- `LIST`: List all launch templates +- `SEARCH`: Search for launch templates by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-managed-prefix-list.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-managed-prefix-list.md new file mode 100644 index 00000000..39681af9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-managed-prefix-list.md @@ -0,0 +1,18 @@ +--- +title: Managed Prefix List +sidebar_label: ec2-managed-prefix-list +--- + +A managed prefix list is a set of one or more CIDR blocks that you can reference in security group rules, route table routes, and other network configuration. Transit gateway routes can use a prefix list as the destination instead of a single CIDR. + +Official API documentation: [DescribeManagedPrefixLists](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeManagedPrefixLists.html) + +**Terraform Mappings:** + +- `aws_ec2_managed_prefix_list.id` + +## Supported Methods + +- `GET`: Get a managed prefix list by ID +- `LIST`: List all managed prefix lists +- `SEARCH`: Search by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-nat-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-nat-gateway.md new file mode 100644 index 00000000..e3450d15 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-nat-gateway.md @@ -0,0 +1,35 @@ +--- +title: NAT Gateway +sidebar_label: ec2-nat-gateway +--- + +A NAT Gateway is an AWS managed network appliance that enables instances in a private subnet to initiate outbound IPv4 (and, in the case of an **NAT Gateway (v2)**, IPv6) traffic to the internet or other AWS services, while preventing unsolicited inbound connections from the public internet. It provides higher bandwidth and easier management compared to NAT instances, and is designed to be highly available within an Availability Zone. +For a full description of its features and limitations, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html + +**Terrafrom Mappings:** + +- `aws_nat_gateway.id` + +## Supported Methods + +- `GET`: Get a NAT Gateway by ID +- `LIST`: List all NAT gateways +- `SEARCH`: Search for NAT gateways by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The NAT Gateway is always created inside a specific VPC; this link lets you trace which virtual network the gateway belongs to. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A NAT Gateway is placed in exactly one subnet. This link shows the subnet that hosts the gateway’s elastic network interface. + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +Each NAT Gateway is automatically assigned an elastic network interface (ENI). Following this link reveals the ENI that represents the gateway inside the subnet. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +When you create a NAT Gateway you must allocate at least one Elastic IP address. This link connects the gateway to the public IP(s) it advertises to the internet. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-network-acl.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-acl.md new file mode 100644 index 00000000..2752cf06 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-acl.md @@ -0,0 +1,27 @@ +--- +title: Network ACL +sidebar_label: ec2-network-acl +--- + +A Network Access Control List (ACL) is a stateless, virtual firewall that controls inbound and outbound traffic at the subnet boundary within an Amazon Virtual Private Cloud (VPC). Each rule in a Network ACL is evaluated in order, enabling or denying traffic based on protocol, port range and source or destination IP. Unlike security groups, Network ACLs apply to all resources inside the associated subnets, making them a coarse-grained layer of network security. +For full details, see the AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-network-acls.html + +**Terrafrom Mappings:** + +- `aws_network_acl.id` + +## Supported Methods + +- `GET`: Get a network ACL +- `LIST`: List all network ACLs +- `SEARCH`: Search for network ACLs by ARN + +## Possible Links + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A Network ACL is attached to one or more subnets; traffic entering or leaving those subnets is evaluated against the ACL’s rule set. Overmind therefore links an `ec2-network-acl` to the `ec2-subnet` resources it governs. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Every Network ACL exists inside a single VPC. Overmind links an `ec2-network-acl` to its parent `ec2-vpc` to show the broader network context in which the ACL operates. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface-permission.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface-permission.md new file mode 100644 index 00000000..7eccd3a2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface-permission.md @@ -0,0 +1,19 @@ +--- +title: Network Interface Permission +sidebar_label: ec2-network-interface-permission +--- + +An EC2 **Network Interface Permission** represents the right of an AWS principal (usually another AWS account) to attach a specific Elastic Network Interface (ENI) to an instance in that principal’s account. By creating or revoking these permissions you can share network interfaces across accounts in a controlled manner without transferring ownership. +Further information can be found in the AWS official documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_NetworkInterfacePermission.html + +## Supported Methods + +- `GET`: Get a network interface permission by ID +- `LIST`: List all network interface permissions +- `SEARCH`: Search network interface permissions by ARN + +## Possible Links + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +A network interface permission is always associated with a single network interface; the linked `ec2-network-interface` item is the ENI to which this permission applies. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface.md new file mode 100644 index 00000000..3379b135 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface.md @@ -0,0 +1,43 @@ +--- +title: EC2 Network Interface +sidebar_label: ec2-network-interface +--- + +An Amazon Elastic Compute Cloud (EC2) Network Interface – often referred to as an Elastic Network Interface (ENI) – is a virtual network card that can be attached to an EC2 instance. It provides the instance with connectivity within a Virtual Private Cloud (VPC) and, optionally, to the public Internet. Each ENI contains a primary private IPv4 address, one or more secondary IPv4 addresses, IPv6 addresses if enabled, one or more security groups, a MAC address, and, when required, an Elastic IP address or a public DNS name. ENIs can be moved between instances, created in advance, or used for high-availability network configurations such as dual-homed instances. +For complete details see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html + +**Terrafrom Mappings:** + +- `aws_network_interface.id` + +## Supported Methods + +- `GET`: Get a network interface by ID +- `LIST`: List all network interfaces +- `SEARCH`: Search network interfaces by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +An ENI can be attached to an EC2 instance, providing that instance with network connectivity. Overmind links the interface to the instance(s) it is or has been attached to. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Each ENI is associated with one or more security groups. These groups define the inbound and outbound traffic rules applied at the interface level. The link shows which security groups control traffic for the ENI. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The ENI owns one or more IP addresses (private IPv4, secondary IPv4, IPv6, and optionally Elastic IP). This relationship exposes the individual IP resources attached to the interface. + +### [`dns`](/sources/stdlib/Types/dns) + +If an ENI has a public IPv4 address, AWS automatically creates a corresponding public DNS name; private DNS names may also be present within the VPC. Overmind links these DNS records to the ENI. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +An ENI is created inside a specific subnet. The subnet determines the address range from which the ENI’s private IPs are allocated and the availability zone in which it resides. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Every ENI exists within a single VPC, inheriting that VPC’s routing tables, DHCP options, and network ACLs. This link shows the parent VPC for the interface. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-placement-group.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-placement-group.md new file mode 100644 index 00000000..7364f9bc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-placement-group.md @@ -0,0 +1,16 @@ +--- +title: Placement Group +sidebar_label: ec2-placement-group +--- + +An EC2 Placement Group is an AWS construct that lets you influence how Elastic Compute Cloud (EC2) instances are positioned on the underlying hardware. By creating a placement group with a strategy of `cluster`, `spread`, or `partition`, you can optimise for high-bandwidth, low-latency networking, reduce the risk of simultaneous hardware failures, or isolate groups of instances from one another. For full details, refer to the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html + +**Terrafrom Mappings:** + +- `aws_placement_group.id` + +## Supported Methods + +- `GET`: Get a placement group by ID +- `LIST`: List all placement groups +- `SEARCH`: Search for placement groups by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-reserved-instance.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-reserved-instance.md new file mode 100644 index 00000000..26ac06ef --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-reserved-instance.md @@ -0,0 +1,13 @@ +--- +title: Reserved EC2 Instance +sidebar_label: ec2-reserved-instance +--- + +An AWS Reserved EC2 Instance represents a pre-paid or partially pre-paid commitment to run a specific instance type in a given Availability Zone or Region for a fixed term (one or three years). By committing up-front, you can obtain a significant discount compared with on-demand pricing, but you also take on the risk of paying for capacity you might not end up using. Overmind treats each Reserved Instance as its own resource so that you can surface any financial or capacity-planning risk associated with your reservation portfolio before a deployment is made. +For detailed information on how Reserved Instances work, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/reserved-instances.html + +## Supported Methods + +- `GET`: Get a reserved EC2 instance by ID +- `LIST`: List all reserved EC2 instances +- `SEARCH`: Search reserved EC2 instances by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-route-table.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-route-table.md new file mode 100644 index 00000000..6b29e850 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-route-table.md @@ -0,0 +1,58 @@ +--- +title: Route Table +sidebar_label: ec2-route-table +--- + +A Route Table in Amazon Virtual Private Cloud (VPC) contains a set of rules, called routes, that determine where network traffic is directed. Each route specifies a destination CIDR block and a target (for example, an Internet Gateway, NAT Gateway, network interface or VPC peering connection). AWS evaluates the routes in the table to decide how packets that leave a subnet are forwarded. A VPC can have multiple route tables, allowing you to implement fine-grained traffic segregation and control. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Route_Tables.html + +**Terrafrom Mappings:** + +- `aws_route_table.id` +- `aws_route_table_association.route_table_id` +- `aws_default_route_table.default_route_table_id` +- `aws_route.route_table_id` + +## Supported Methods + +- `GET`: Get a route table by ID +- `LIST`: List all route tables +- `SEARCH`: Search route tables by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The Route Table is created inside a specific VPC; every table therefore has a one-to-one parent relationship with the VPC in which it resides. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +Subnets are associated with a Route Table. Traffic that originates in a subnet is evaluated against the routes in its associated table. One route table can be linked to many subnets. + +### [`ec2-internet-gateway`](/sources/aws/Types/ec2-internet-gateway) + +A Route Table may contain a route whose target is an Internet Gateway, enabling outbound IPv4 traffic (and inbound responses) for the subnets that use the table. + +### [`ec2-vpc-endpoint`](/sources/aws/Types/ec2-vpc-endpoint) + +Interface and Gateway VPC Endpoints can appear as route targets, directing traffic destined for AWS services or private resources through the endpoint. + +### [`ec2-egress-only-internet-gateway`](/sources/aws/Types/ec2-egress-only-internet-gateway) + +For IPv6 connectivity, a Route Table can include a route to an Egress-only Internet Gateway, allowing outbound-only IPv6 traffic from the associated subnets. + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +An individual EC2 instance can be specified as the route target (using its instance ID) when it is acting as a virtual appliance or host-based router. + +### [`ec2-nat-gateway`](/sources/aws/Types/ec2-nat-gateway) + +Routes can target a NAT Gateway, providing Internet access for private subnets while keeping the source IP addresses of instances hidden from the public Internet. + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +A specific Elastic Network Interface (ENI) may be used as a route target to forward traffic to appliances such as firewalls or load balancers hosted on that interface. + +### [`ec2-vpc-peering-connection`](/sources/aws/Types/ec2-vpc-peering-connection) + +When traffic needs to flow between two VPCs, a route whose target is a VPC Peering Connection is added to the Route Table, enabling cross-VPC communication. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group-rule.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group-rule.md new file mode 100644 index 00000000..74c84a62 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group-rule.md @@ -0,0 +1,25 @@ +--- +title: Security Group Rule +sidebar_label: ec2-security-group-rule +--- + +A Security Group Rule represents a single ingress or egress rule that belongs to an Amazon EC2 Security Group. Each rule specifies the protocol, port range, source or destination (IP range, prefix list, security group or prefix), and (optionally) a description that determines whether specific network traffic is allowed to reach, or leave, the resources associated with the parent security group. By analysing these rules, Overmind can surface unintended exposure, overly-permissive access, or conflicts before the configuration is deployed. +For full details see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/security-group-rules.html + +**Terrafrom Mappings:** + +- `aws_security_group_rule.security_group_rule_id` +- `aws_vpc_security_group_ingress_rule.security_group_rule_id` +- `aws_vpc_security_group_egress_rule.security_group_rule_id` + +## Supported Methods + +- `GET`: Get a security group rule by ID +- `LIST`: List all security group rules +- `SEARCH`: Search security group rules by ARN + +## Possible Links + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Every Security Group Rule belongs to exactly one Security Group; Overmind links the rule back to its parent security group so you can trace how an individual rule contributes to the overall ingress or egress policy applied to your instances and other resources. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group.md new file mode 100644 index 00000000..ad1fc210 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group.md @@ -0,0 +1,23 @@ +--- +title: Security Group +sidebar_label: ec2-security-group +--- + +An Amazon EC2 Security Group acts as a virtual firewall that regulates inbound and outbound traffic for resources such as EC2 instances, load balancers, and network interfaces within a Virtual Private Cloud (VPC). Rules are stateful, meaning that return traffic is automatically allowed, and can be specified by protocol, port range, and source or destination (CIDR block, prefix list, or another security group). For further details, refer to the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html + +**Terrafrom Mappings:** + +- `aws_security_group.id` +- `aws_security_group_rule.security_group_id` + +## Supported Methods + +- `GET`: Get a security group by ID +- `LIST`: List all security groups +- `SEARCH`: Search for security groups by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Each security group is created within a single VPC, inherits its CIDR boundaries, and can only be attached to resources that also reside in that VPC. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-snapshot.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-snapshot.md new file mode 100644 index 00000000..513ed38b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-snapshot.md @@ -0,0 +1,19 @@ +--- +title: EC2 Snapshot +sidebar_label: ec2-snapshot +--- + +An Amazon EBS (Elastic Block Store) snapshot is an incremental, point-in-time backup of an EBS volume. Snapshots are stored in Amazon S3 and can be used to restore the original volume, create new volumes in the same or different Availability Zones, and copy data across Regions. They form a key part of disaster-recovery and migration workflows, allowing users to preserve data durability and quickly re-provision storage. +Official documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-creating-snapshot.html + +## Supported Methods + +- `GET`: Get a snapshot by ID +- `LIST`: List all snapshots +- `SEARCH`: Search snapshots by ARN + +## Possible Links + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +A snapshot is created from, and can later be used to recreate, an EBS volume. Overmind links each `ec2-snapshot` to the `ec2-volume` it originated from (and, where relevant, the volumes restored from it), enabling you to trace data lineage and understand the blast radius of any change to the underlying storage. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-subnet.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-subnet.md new file mode 100644 index 00000000..4822a8cd --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-subnet.md @@ -0,0 +1,24 @@ +--- +title: EC2 Subnet +sidebar_label: ec2-subnet +--- + +An EC2 subnet is a logically isolated section of an Amazon Virtual Private Cloud that lets you group resources together and control how traffic flows to and from them. Each subnet resides in a single Availability Zone, inherits the VPC’s CIDR range, and can be configured as public or private depending on whether its routing table points traffic to an Internet Gateway or not. Subnets form the basic building blocks for networking in AWS, determining IP addressing, network reachability, and security-group/network-ACL boundaries. +For full details see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html + +**Terrafrom Mappings:** + +- `aws_route_table_association.subnet_id` +- `aws_subnet.id` + +## Supported Methods + +- `GET`: Get a subnet by ID +- `LIST`: List all subnets +- `SEARCH`: Search for subnets by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Every subnet must belong to exactly one VPC. This relationship allows Overmind to trace how traffic is routed from the subnet through VPC-level components such as Internet Gateways, NAT Gateways, route tables, and network ACLs. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-attachment.md new file mode 100644 index 00000000..5b706d45 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-attachment.md @@ -0,0 +1,20 @@ +--- +title: Transit Gateway Attachment +sidebar_label: ec2-transit-gateway-attachment +--- + +A Transit Gateway attachment connects a resource (VPC, VPN connection, Direct Connect gateway, peering connection, or Connect attachment) to a transit gateway. Attachments are associated with route tables and can have routes propagated to them. + +Official API documentation: [DescribeTransitGatewayAttachments](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayAttachments.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_vpc_attachment.id` (VPC) +- `aws_ec2_transit_gateway_vpn_attachment.id` (VPN) +- Other attachment types have corresponding Terraform resources. + +## Supported Methods + +- `GET`: Get a transit gateway attachment by ID +- `LIST`: List all transit gateway attachments +- `SEARCH`: Search by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-announcement.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-announcement.md new file mode 100644 index 00000000..878175d5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-announcement.md @@ -0,0 +1,12 @@ +--- +title: Transit Gateway Route Table Announcement +sidebar_label: ec2-transit-gateway-route-table-announcement +--- + +A Transit Gateway Route Table Announcement represents the advertisement of a transit gateway route table to a peer—for example, to another transit gateway (peering) or to an AWS Network Manager core network. Routes that originate from such an announcement appear in the route table with a `TransitGatewayRouteTableAnnouncementId`, and Overmind links those [ec2-transit-gateway-route](/sources/aws/Types/ec2-transit-gateway-route) items to this type. + +Official API documentation: [DescribeTransitGatewayRouteTableAnnouncements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTableAnnouncements.html) + +## Note + +Overmind does not currently provide a dedicated adapter for `ec2-transit-gateway-route-table-announcement`. This type is documented because [ec2-transit-gateway-route](/sources/aws/Types/ec2-transit-gateway-route) items can link to it when a route originates from a route table announcement (`TransitGatewayRouteTableAnnouncementId`). diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-association.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-association.md new file mode 100644 index 00000000..e5828353 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-association.md @@ -0,0 +1,40 @@ +--- +title: Transit Gateway Route Table Association +sidebar_label: ec2-transit-gateway-route-table-association +--- + +An association links a transit gateway attachment (VPC, VPN, Direct Connect gateway, peering, or Connect) to a transit gateway route table. Traffic for that attachment is routed according to the route table. + +Official API documentation: [GetTransitGatewayRouteTableAssociations](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetTransitGatewayRouteTableAssociations.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_route_table_association.id` + +## Supported Methods + +- `GET`: Get by composite ID `TransitGatewayRouteTableId|TransitGatewayAttachmentId` +- `LIST`: List all route table associations (across all route tables in the scope) +- `SEARCH`: Search by `TransitGatewayRouteTableId` to list all associations for that route table (used by the route table’s link to associations) + +## Possible Links + +### [`ec2-transit-gateway-route-table`](/sources/aws/Types/ec2-transit-gateway-route-table) + +The route table that the attachment is associated with. + +### [`ec2-transit-gateway-attachment`](/sources/aws/Types/ec2-transit-gateway-attachment) + +The transit gateway attachment that is associated with the route table. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +When the attachment resource type is VPC, the linked VPC. + +### [`ec2-vpn-connection`](/sources/aws/Types/ec2-vpn-connection) + +When the attachment resource type is VPN, the linked VPN connection. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +When the attachment resource type is Direct Connect gateway, the linked Direct Connect gateway. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-propagation.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-propagation.md new file mode 100644 index 00000000..ed68c34f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-propagation.md @@ -0,0 +1,44 @@ +--- +title: Transit Gateway Route Table Propagation +sidebar_label: ec2-transit-gateway-route-table-propagation +--- + +A propagation enables a transit gateway route table to automatically learn routes from an attachment (VPC, VPN, Direct Connect gateway, peering, or Connect). When propagation is enabled, routes from that attachment appear in the route table. + +Official API documentation: [GetTransitGatewayRouteTablePropagations](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetTransitGatewayRouteTablePropagations.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_route_table_propagation.id` + +## Supported Methods + +- `GET`: Get by composite ID `TransitGatewayRouteTableId|TransitGatewayAttachmentId` +- `LIST`: List all route table propagations (across all route tables in the scope) +- `SEARCH`: Search by `TransitGatewayRouteTableId` to list all propagations for that route table (used by the route table’s link to propagations) + +## Possible Links + +### [`ec2-transit-gateway-route-table`](/sources/aws/Types/ec2-transit-gateway-route-table) + +The route table that is propagating routes from the attachment. + +### [`ec2-transit-gateway-route-table-association`](/sources/aws/Types/ec2-transit-gateway-route-table-association) + +The route table association for the same route table and attachment (same composite ID). Links propagation and association in the graph. + +### [`ec2-transit-gateway-attachment`](/sources/aws/Types/ec2-transit-gateway-attachment) + +The attachment whose routes are being propagated into the route table. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +When the attachment resource type is VPC, the linked VPC. + +### [`ec2-vpn-connection`](/sources/aws/Types/ec2-vpn-connection) + +When the attachment resource type is VPN, the linked VPN connection. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +When the attachment resource type is Direct Connect gateway, the linked Direct Connect gateway. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table.md new file mode 100644 index 00000000..f0efd09c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table.md @@ -0,0 +1,36 @@ +--- +title: Transit Gateway Route Table +sidebar_label: ec2-transit-gateway-route-table +--- + +A Transit Gateway Route Table determines how traffic is routed for attachments (VPCs, VPNs, Direct Connect gateways, peering connections, or Connect attachments) that are associated with it. Each transit gateway has a default route table; you can create additional route tables to control which attachments can reach which routes. + +Official API documentation: [DescribeTransitGatewayRouteTables](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_route_table.id` + +## Supported Methods + +- `GET`: Get a transit gateway route table by ID +- `LIST`: List all transit gateway route tables +- `SEARCH`: Search transit gateway route tables by ARN + +## Possible Links + +### [`ec2-transit-gateway`](/sources/aws/Types/ec2-transit-gateway) + +Each transit gateway route table belongs to a single transit gateway. The route table controls routing for attachments that are associated with it. + +### [`ec2-transit-gateway-route-table-association`](/sources/aws/Types/ec2-transit-gateway-route-table-association) + +Associations for this route table (Search by route table ID). Each association links an attachment to this route table. + +### [`ec2-transit-gateway-route-table-propagation`](/sources/aws/Types/ec2-transit-gateway-route-table-propagation) + +Propagations for this route table (Search by route table ID). Each propagation enables the route table to learn routes from an attachment. + +### [`ec2-transit-gateway-route`](/sources/aws/Types/ec2-transit-gateway-route) + +Routes in this route table (Search by route table ID). diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route.md new file mode 100644 index 00000000..361596ce --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route.md @@ -0,0 +1,52 @@ +--- +title: Transit Gateway Route +sidebar_label: ec2-transit-gateway-route +--- + +A route in a transit gateway route table. Each route has a destination (CIDR or prefix list) and a target (attachment or resource). Routes can be static or propagated from attachments. + +Official API documentation: [SearchTransitGatewayRoutes](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SearchTransitGatewayRoutes.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_route.id` + +## Supported Methods + +- `GET`: Get by composite ID `TransitGatewayRouteTableId|Destination`, where Destination is a CIDR (e.g. `10.0.0.0/16`) or prefix list (e.g. `pl:PrefixListId`) +- `LIST`: List all transit gateway routes (across all route tables in the scope) +- `SEARCH`: Search by `TransitGatewayRouteTableId` to list all routes in that route table (used by the route table’s link to routes) + +## Possible Links + +### [`ec2-transit-gateway-route-table`](/sources/aws/Types/ec2-transit-gateway-route-table) + +The route table that contains this route. + +### [`ec2-transit-gateway-route-table-association`](/sources/aws/Types/ec2-transit-gateway-route-table-association) + +For each attachment that this route targets, the corresponding route table association (same route table and attachment). Links routes and associations in the graph. + +### [`ec2-transit-gateway-attachment`](/sources/aws/Types/ec2-transit-gateway-attachment) + +Each attachment that this route targets (from the route’s `TransitGatewayAttachments`). + +### [`ec2-transit-gateway-route-table-announcement`](/sources/aws/Types/ec2-transit-gateway-route-table-announcement) + +When the route originates from a route table announcement, the linked transit gateway route table announcement. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +When a route attachment’s resource type is VPC, the linked VPC. + +### [`ec2-vpn-connection`](/sources/aws/Types/ec2-vpn-connection) + +When a route attachment’s resource type is VPN, the linked VPN connection. + +### [`ec2-managed-prefix-list`](/sources/aws/Types/ec2-managed-prefix-list) + +When the route destination is a prefix list (instead of a CIDR), the managed prefix list. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +When a route attachment’s resource type is Direct Connect gateway, the linked Direct Connect gateway. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway.md new file mode 100644 index 00000000..70ade350 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway.md @@ -0,0 +1,18 @@ +--- +title: Transit Gateway +sidebar_label: ec2-transit-gateway +--- + +An AWS Transit Gateway is a network transit hub that you use to interconnect your VPCs and on-premises networks. Each transit gateway has a default route table and can have additional route tables to control routing for attachments (VPCs, VPNs, Direct Connect gateways, peering, Connect). + +Official API documentation: [DescribeTransitGateways](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGateways.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway.id` + +## Supported Methods + +- `GET`: Get a transit gateway by ID +- `LIST`: List all transit gateways +- `SEARCH`: Search transit gateways by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-volume-status.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-volume-status.md new file mode 100644 index 00000000..0a8becbb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-volume-status.md @@ -0,0 +1,19 @@ +--- +title: EC2 Volume Status +sidebar_label: ec2-volume-status +--- + +The EC2 Volume Status resource represents the health information that AWS exposes for every Amazon Elastic Block Store (EBS) volume. Derived from the `DescribeVolumeStatus` API call, it records the results of automated status checks, any events that might affect I/O, and recommended user actions. Monitoring these objects in Overmind lets you spot degraded or impaired volumes before they compromise a deployment. +For a complete description of the data returned by AWS, see the official documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVolumeStatus.html + +## Supported Methods + +- `GET`: Get a volume status by volume ID +- `LIST`: List all volume statuses +- `SEARCH`: Search for volume statuses by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +A Volume Status relates to the EC2 instance that the underlying EBS volume is currently attached to, if any. Overmind links the status object to the instance so you can trace how a failing or impaired volume might impact the workloads running on that instance. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-volume.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-volume.md new file mode 100644 index 00000000..ab7b8ee3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-volume.md @@ -0,0 +1,22 @@ +--- +title: EC2 Volume +sidebar_label: ec2-volume +--- + +An Amazon Elastic Block Store (EBS) volume provides persistent block-level storage for use with Amazon EC2 instances. Volumes can be attached to a single instance at a time (or multiple instances when using Multi-Attach), and retain their data independently of the life-cycle of that instance. Sizes, performance characteristics and encryption settings are configurable, allowing teams to tailor storage to the workload’s needs. Full service behaviour is documented by AWS here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumes.html + +**Terrafrom Mappings:** + +- `aws_ebs_volume.id` + +## Supported Methods + +- `GET`: Get a volume by ID +- `LIST`: List all volumes +- `SEARCH`: Search volumes by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +A volume may be attached to, detached from or created alongside an EC2 instance. Overmind links the two resources so you can trace how storage changes could affect, or be affected by, the compute resource that consumes it. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-endpoint.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-endpoint.md new file mode 100644 index 00000000..5a2596b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-endpoint.md @@ -0,0 +1,16 @@ +--- +title: VPC Endpoint +sidebar_label: ec2-vpc-endpoint +--- + +A VPC Endpoint is an elastic network interface or gateway that enables private connectivity between resources inside an Amazon Virtual Private Cloud (VPC) and supported AWS or third-party services, without traversing the public internet. By routing traffic through the AWS network, VPC Endpoints improve security, reduce latency and remove the need for NAT devices, VPNs or Direct Connect links. For full details, see the AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-endpoints.html + +**Terrafrom Mappings:** + +- `aws_vpc_endpoint.id` + +## Supported Methods + +- `GET`: Get a VPC Endpoint by ID +- `LIST`: List all VPC Endpoints +- `SEARCH`: Search VPC Endpoints by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-peering-connection.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-peering-connection.md new file mode 100644 index 00000000..a108c04b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-peering-connection.md @@ -0,0 +1,25 @@ +--- +title: VPC Peering Connection +sidebar_label: ec2-vpc-peering-connection +--- + +A VPC Peering Connection enables you to route traffic privately between two Virtual Private Clouds (VPCs) without traversing the public internet. Peering can be established between VPCs in the same AWS account or across different AWS accounts, and—subject to region support—across regions. It is commonly used for micro-service communication, shared services networks, or multi-account architectures where low-latency, high-bandwidth connectivity with AWS-managed security controls is required. +For full details, refer to the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html + +**Terrafrom Mappings:** + +- `aws_vpc_peering_connection.id` +- `aws_vpc_peering_connection_accepter.id` +- `aws_vpc_peering_connection_options.vpc_peering_connection_id` + +## Supported Methods + +- `GET`: Get a VPC Peering Connection by ID +- `LIST`: List all VPC Peering Connections +- `SEARCH`: Search for VPC Peering Connections by their ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Each VPC Peering Connection has exactly two endpoints—a requester VPC and an accepter VPC. Linking to the `ec2-vpc` resource allows Overmind to show which VPCs are joined by a given peering connection and, conversely, which peering connections a particular VPC participates in. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc.md new file mode 100644 index 00000000..54132959 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc.md @@ -0,0 +1,16 @@ +--- +title: VPC +sidebar_label: ec2-vpc +--- + +An Amazon Virtual Private Cloud (VPC) is a logically isolated section of AWS in which you can launch and manage AWS resources within a virtual network that you define. Within a VPC you control IP address ranges, subnets, route tables, network gateways, security groups, and network ACLs, allowing you to shape how traffic flows to and from your workloads while keeping them isolated from, or connected to, the public Internet and other VPCs as required. For a full overview, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html. + +**Terrafrom Mappings:** + +- `aws_vpc.id` + +## Supported Methods + +- `GET`: Get a VPC by ID +- `LIST`: List all VPCs +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-vpn-connection.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpn-connection.md new file mode 100644 index 00000000..46bf1cdc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpn-connection.md @@ -0,0 +1,18 @@ +--- +title: VPN Connection +sidebar_label: ec2-vpn-connection +--- + +An AWS Site-to-Site VPN connection links your on-premises network to your VPC (or to a transit gateway) over an encrypted IPsec tunnel. VPN connections can be attached to a transit gateway for use in a hub-and-spoke topology. + +Official API documentation: [DescribeVpnConnections](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpnConnections.html) + +**Terraform Mappings:** + +- `aws_vpn_connection.id` + +## Supported Methods + +- `GET`: Get a VPN connection by ID +- `LIST`: List all VPN connections +- `SEARCH`: Search by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-capacity-provider.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-capacity-provider.md new file mode 100644 index 00000000..be279e71 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-capacity-provider.md @@ -0,0 +1,23 @@ +--- +title: Capacity Provider +sidebar_label: ecs-capacity-provider +--- + +An Amazon ECS capacity provider tells a cluster where its compute capacity comes from and how that capacity should scale. It can point to an Auto Scaling group of EC2 instances or to the serverless Fargate/Fargate Spot capacity pools, and it contains rules that determine when and how instances are launched or terminated to satisfy task demand. Using capacity providers allows platform teams to separate scaling logic from task scheduling and to adopt multiple capacity sources within a single cluster. +For complete details see the official AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-capacity-providers.html + +**Terrafrom Mappings:** + +- `aws_ecs_capacity_provider.arn` + +## Supported Methods + +- `GET`: Get a capacity provider by its short name or full Amazon Resource Name (ARN). +- `LIST`: List capacity providers. +- `SEARCH`: Search capacity providers by ARN + +## Possible Links + +### [`autoscaling-auto-scaling-group`](/sources/aws/Types/autoscaling-auto-scaling-group) + +A capacity provider that is backed by EC2 instances references exactly one Auto Scaling group. The link lets you trace from the capacity provider to the group that actually supplies instances, making it easy to understand which fleet of instances will scale in response to ECS task demand and to assess risks such as insufficient instance types, mis-configured scaling policies, or conflicting lifecycle hooks. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-cluster.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-cluster.md new file mode 100644 index 00000000..937d1c8d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-cluster.md @@ -0,0 +1,35 @@ +--- +title: ECS Cluster +sidebar_label: ecs-cluster +--- + +An Amazon ECS (Elastic Container Service) cluster is a logical grouping of tasks or services. It acts as the fundamental boundary for scheduling, networking and capacity management in ECS: every task or service is launched into exactly one cluster, and the cluster manages the resources on which containers run. +For full details see the official AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html + +**Terrafrom Mappings:** + +- `aws_ecs_cluster.arn` + +## Supported Methods + +- `GET`: Get a cluster by name +- `LIST`: List all clusters +- `SEARCH`: Search for a cluster by ARN + +## Possible Links + +### [`ecs-container-instance`](/sources/aws/Types/ecs-container-instance) + +An ECS cluster is composed of zero or more container instances (EC2 hosts or AWS Fargate-managed capacity). Each `ecs-container-instance` record represents a specific compute resource that has registered itself to the cluster and is available for running tasks. + +### [`ecs-service`](/sources/aws/Types/ecs-service) + +Services define long-running workloads that are maintained by ECS within the cluster. Every `ecs-service` is created inside a particular cluster and relies on the cluster’s scheduler to place and maintain tasks according to the service definition. + +### [`ecs-task`](/sources/aws/Types/ecs-task) + +Tasks are the running instantiations of container definitions. When a task is started, it is launched into a specific cluster; therefore every `ecs-task` is linked back to the cluster that provided the capacity and networking for it. + +### [`ecs-capacity-provider`](/sources/aws/Types/ecs-capacity-provider) + +Capacity providers control how ECS acquires compute capacity for a cluster (e.g. Fargate, Auto Scaling groups). A cluster may have one or more `ecs-capacity-provider` resources associated with it, and those associations determine how tasks and services within the cluster obtain the underlying compute resources they require. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-container-instance.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-container-instance.md new file mode 100644 index 00000000..0d0c65d4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-container-instance.md @@ -0,0 +1,18 @@ +--- +title: Container Instance +sidebar_label: ecs-container-instance +--- + +A container instance represents an Amazon EC2 host that has been registered to an Amazon ECS cluster and is therefore available for running one or more ECS tasks. Each container instance runs the ECS agent and reports its status, resource availability and running tasks back to the cluster’s control plane. For a detailed explanation of container instances, provisioning requirements, and lifecycle behaviour, see the official AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html + +## Supported Methods + +- `GET`: Get a container instance by ID which consists of `{clusterName}/{id}` +- ~~`LIST`~~ +- `SEARCH`: Search for container instances by cluster + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +Every container instance is physically an Amazon EC2 instance. Linking to the `ec2-instance` type allows Overmind to surface the underlying compute resource, including its security groups, IAM roles and network configuration, all of which can influence the risk profile of the container instance and any tasks scheduled on it. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-service.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-service.md new file mode 100644 index 00000000..4b22455b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-service.md @@ -0,0 +1,43 @@ +--- +title: ECS Service +sidebar_label: ecs-service +--- + +An Amazon Elastic Container Service (ECS) **service** is the long-running, scalable unit that maintains a specified number of copies of a task definition running on an ECS cluster. The service schedules tasks either on EC2 instances or on Fargate, monitors their health, replaces unhealthy tasks and, when configured, integrates with Elastic Load Balancing and AWS Service Discovery. +For a full description see the AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html + +**Terrafrom Mappings:** + +- `aws_ecs_service.cluster_name` + +## Supported Methods + +- `GET`: Get an ECS service by full name (`{clusterName}/{id}`) +- ~~`LIST`~~ +- `SEARCH`: Search for ECS services by cluster + +## Possible Links + +### [`ecs-cluster`](/sources/aws/Types/ecs-cluster) + +The service is deployed into exactly one ECS cluster, so each ecs-service will have a **`parent`** relationship to the corresponding `ecs-cluster`. + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +If the service is configured with a load balancer, it registers its tasks as targets in one or more ELBv2 target groups; Overmind creates a **`uses`** link from the service to every target group referenced in its loadBalancer or serviceConnect configuration. + +### [`ecs-task-definition`](/sources/aws/Types/ecs-task-definition) + +A service runs a specific revision of a task definition. There is therefore a **`depends_on`** link from the service to the task definition ARN specified in `taskDefinition`. + +### [`ecs-capacity-provider`](/sources/aws/Types/ecs-capacity-provider) + +When a capacity provider strategy is attached, the service relies on one or more capacity providers for scheduling. Overmind shows a **`uses`** link to each referenced `ecs-capacity-provider`. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +For services that use the `awsvpc` network mode (Fargate or ENI-aware EC2 launch type), the service’s tasks are launched inside specific subnets defined in the service’s network configuration; those subnets are exposed via **`uses`** links. + +### [`dns`](/sources/stdlib/Types/dns) + +If AWS Cloud Map service discovery is enabled, the ECS service automatically creates DNS records (A, AAAA, or SRV) for its tasks. Overmind surfaces a **`creates`** link to the resultant DNS names. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-task-definition.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-task-definition.md new file mode 100644 index 00000000..3df84160 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-task-definition.md @@ -0,0 +1,27 @@ +--- +title: Task Definition +sidebar_label: ecs-task-definition +--- + +An Amazon ECS task definition is the blueprint that tells AWS ECS how to run one or more containers. It specifies details such as the container images, CPU and memory requirements, networking mode, logging configuration, IAM roles, and secrets that should be injected into the containers. Each time you register a new version, ECS creates a new immutable revision that can be referenced directly or through the family name. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html + +**Terrafrom Mappings:** + +- `aws_ecs_task_definition.family` + +## Supported Methods + +- `GET`: Get a task definition by revision name (`{family}:{revision}`) +- `LIST`: List all task definitions +- `SEARCH`: Search for task definitions by ARN + +## Possible Links + +### [`iam-role`](/sources/aws/Types/iam-role) + +A task definition can reference an IAM role through `taskRoleArn` and/or `executionRoleArn`. These roles grant the running containers the permissions they need to interact with other AWS services or to pull private images and write logs. Overmind links the task definition to the IAM role resources so you can see the exact permissions that will be in effect at runtime. + +### [`ssm-parameter`](/sources/aws/Types/ssm-parameter) + +Environment variables or secrets defined in a task definition can be sourced from AWS Systems Manager Parameter Store. Whenever a task definition lists an SSM parameter (e.g., via the `secrets` block), Overmind surfaces a link to the corresponding `ssm-parameter` item, allowing you to trace where sensitive configuration values originate and assess the impact of changes. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-task.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-task.md new file mode 100644 index 00000000..1d78ca8d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-task.md @@ -0,0 +1,35 @@ +--- +title: ECS Task +sidebar_label: ecs-task +--- + +An ECS task is the fundamental unit of work that runs on Amazon Elastic Container Service (ECS). It represents one instantiation of a task definition: a group of one or more Docker containers that are deployed together on the same host. A task lives within an ECS cluster and may run on EC2 instances or on AWS Fargate. The task record captures runtime information such as status, start/stop times, allocated network interfaces and resource utilisation. +For full details, see the AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_tasks.html + +## Supported Methods + +- `GET`: Get an ECS task by ID +- ~~`LIST`~~ +- `SEARCH`: Search for ECS tasks by cluster + +## Possible Links + +### [`ecs-cluster`](/sources/aws/Types/ecs-cluster) + +The task is launched inside exactly one ECS cluster, so Overmind links each task back to the cluster that owns it. + +### [`ecs-container-instance`](/sources/aws/Types/ecs-container-instance) + +For tasks that use the EC2 launch type, the task runs on a specific ECS container instance (an EC2 host registered with the cluster). Overmind links the task to the container instance on which it is currently placed. + +### [`ecs-task-definition`](/sources/aws/Types/ecs-task-definition) + +Every task is an instantiation of a task definition. Overmind records this relationship so you can trace configuration changes in the task definition that may affect a running task. + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +When a task uses the `awsvpc` network mode (or is a Fargate task), AWS allocates one or more elastic network interfaces (ENIs) to the task. These ENIs are linked so you can observe associated security groups, subnets and IP addresses. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Each ENI attached to the task is assigned private (and optionally public) IP addresses. Overmind surfaces these IP resources, allowing you to see which IPs are in use by a given task and how they propagate through your network topology. diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-access-point.md b/docs.overmind.tech/docs/sources/aws/Types/efs-access-point.md new file mode 100644 index 00000000..8a0c8cd6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-access-point.md @@ -0,0 +1,17 @@ +--- +title: EFS Access Point +sidebar_label: efs-access-point +--- + +Amazon Elastic File System (EFS) Access Points are application-specific entry points into an EFS file system. Each access point can enforce a unique POSIX user, group and root directory, allowing multiple workloads or tenants to share the same file system while maintaining separation and least-privilege access. Access points are commonly used to simplify permissions when deploying containers, serverless functions or batch jobs that need shared storage. +Official AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html + +**Terrafrom Mappings:** + +- `aws_efs_access_point.id` + +## Supported Methods + +- `GET`: Get an access point by ID +- `LIST`: List all access points +- `SEARCH`: Search for an access point by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-backup-policy.md b/docs.overmind.tech/docs/sources/aws/Types/efs-backup-policy.md new file mode 100644 index 00000000..90c97b0d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-backup-policy.md @@ -0,0 +1,17 @@ +--- +title: EFS Backup Policy +sidebar_label: efs-backup-policy +--- + +An EFS Backup Policy represents the setting on an Amazon Elastic File System (EFS) file system that turns automatic, daily AWS Backup protection on or off. When the policy is enabled, AWS Backup creates incremental backups of the file system and retains them according to the configured backup plan; when it is disabled, the file system is excluded from automated protection. Managing this resource helps ensure that critical data stored in EFS is covered by a consistent backup and retention strategy, reducing the risk of accidental data loss. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/awsbackup.html + +**Terrafrom Mappings:** + +- `aws_efs_backup_policy.id` + +## Supported Methods + +- `GET`: Get an Backup Policy by file system ID +- ~~`LIST`~~ +- `SEARCH`: Search for an Backup Policy by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-file-system.md b/docs.overmind.tech/docs/sources/aws/Types/efs-file-system.md new file mode 100644 index 00000000..5798dd98 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-file-system.md @@ -0,0 +1,16 @@ +--- +title: EFS File System +sidebar_label: efs-file-system +--- + +Amazon Elastic File System (EFS) provides a scalable, elastic and fully-managed Network File System (NFS) that can be mounted concurrently by multiple AWS compute services, including EC2, Lambda and containers. It automatically grows and shrinks as you add or remove data, removing the need to provision storage up front, and offers high availability across multiple Availability Zones. For a full overview, refer to the official AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html + +**Terrafrom Mappings:** + +- `aws_efs_file_system.id` + +## Supported Methods + +- `GET`: Get a file system by ID +- `LIST`: List file systems +- `SEARCH`: Search file systems by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-mount-target.md b/docs.overmind.tech/docs/sources/aws/Types/efs-mount-target.md new file mode 100644 index 00000000..14900d71 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-mount-target.md @@ -0,0 +1,17 @@ +--- +title: EFS Mount Target +sidebar_label: efs-mount-target +--- + +An EFS Mount Target is a network endpoint that resides in a specific subnet inside your VPC and exposes an Amazon Elastic File System (EFS) file system to compute resources such as EC2 instances, ECS tasks, Lambda functions and other AWS services. By creating one mount target in each Availability Zone where the file system will be accessed, you ensure low-latency, highly available access to shared file storage. Each mount target can be associated with one or more security groups, allowing fine-grained control over which clients can connect to the file system. +For further details, refer to the official AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/efs-mount-targets.html + +**Terrafrom Mappings:** + +- `aws_efs_mount_target.id` + +## Supported Methods + +- `GET`: Get an mount target by ID +- ~~`LIST`~~ +- `SEARCH`: Search for mount targets by file system ID diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-replication-configuration.md b/docs.overmind.tech/docs/sources/aws/Types/efs-replication-configuration.md new file mode 100644 index 00000000..3728b8e9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-replication-configuration.md @@ -0,0 +1,17 @@ +--- +title: EFS Replication Configuration +sidebar_label: efs-replication-configuration +--- + +An Amazon Elastic File System (EFS) Replication Configuration defines how an EFS file system is asynchronously replicated to another AWS Region or Availability Zone, providing disaster-recovery protection and enhanced data durability. By creating a replication configuration you specify the source file system, the destination Region, and the encryption and retention settings for the replica. Replication occurs automatically and continuously, with recovery point objectives (RPO) typically within minutes, allowing you to fail over quickly if the primary file system becomes unavailable. +For full details, refer to the AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/efs-replication.html + +**Terrafrom Mappings:** + +- `aws_efs_replication_configuration.source_file_system_id` + +## Supported Methods + +- `GET`: Get a replication configuration by file system ID +- `LIST`: List all replication configurations +- `SEARCH`: Search for a replication configuration by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/eks-addon.md b/docs.overmind.tech/docs/sources/aws/Types/eks-addon.md new file mode 100644 index 00000000..9ddd3e89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/eks-addon.md @@ -0,0 +1,17 @@ +--- +title: EKS Addon +sidebar_label: eks-addon +--- + +An Amazon EKS Addon is an AWS-managed installation of common operational software—such as CoreDNS, kube-proxy, the Amazon VPC CNI plugin or the Amazon EBS CSI driver—onto an Amazon Elastic Kubernetes Service (EKS) cluster. Addons let you declare the component, version and configuration you want, while AWS takes care of deployment, upgrades, security patches and ongoing lifecycle management. Using addons keeps the cluster’s critical services consistent and up to date without manual intervention. +For more information, see the official AWS documentation on EKS Add-ons: https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html + +**Terrafrom Mappings:** + +- `aws_eks_addon.id` + +## Supported Methods + +- `GET`: Get an addon by unique name (`{clusterName}:{addonName}`) +- ~~`LIST`~~ +- `SEARCH`: Search addons by cluster name diff --git a/docs.overmind.tech/docs/sources/aws/Types/eks-cluster.md b/docs.overmind.tech/docs/sources/aws/Types/eks-cluster.md new file mode 100644 index 00000000..a8fc0c25 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/eks-cluster.md @@ -0,0 +1,16 @@ +--- +title: EKS Cluster +sidebar_label: eks-cluster +--- + +Amazon Elastic Kubernetes Service (EKS) is a managed Kubernetes control plane that allows you to run Kubernetes workloads on AWS without the operational overhead of managing the underlying master nodes. An EKS cluster handles tasks such as control-plane provisioning, scalability, high availability and automatic patching, while letting you attach one or more node groups (either managed or self-managed) to run your containerised applications. See the official AWS documentation for full details: https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html + +**Terraform Mappings:** + +- `aws_eks_cluster.arn` + +## Supported Methods + +- `GET`: Get a cluster by name +- `LIST`: List all clusters +- `SEARCH`: Search for clusters by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/eks-fargate-profile.md b/docs.overmind.tech/docs/sources/aws/Types/eks-fargate-profile.md new file mode 100644 index 00000000..3653feb8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/eks-fargate-profile.md @@ -0,0 +1,26 @@ +--- +title: Fargate Profile +sidebar_label: eks-fargate-profile +--- + +An Amazon EKS Fargate profile tells EKS which pods in a cluster should run on AWS Fargate rather than on self-managed or managed EC2 worker nodes. It contains a set of selectors (namespace and optional labels) and the networking configuration (subnets and the pod execution IAM role) that EKS will use when it launches Fargate tasks on your behalf. See the official documentation for full details: https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html + +**Terrafrom Mappings:** + +- `aws_eks_fargate_profile.id` + +## Supported Methods + +- `GET`: Get a fargate profile by unique name (`{clusterName}:{FargateProfileName}`) +- ~~`LIST`~~ +- `SEARCH`: Search for fargate profiles by cluster name + +## Possible Links + +### [`iam-role`](/sources/aws/Types/iam-role) + +Each Fargate profile references a “pod execution role”, an IAM role that grants EKS permission to pull container images and publish pod logs when it provisions the Fargate tasks. Overmind therefore creates a link from the profile to the IAM role specified in `pod_execution_role_arn`. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +The profile’s `subnet_ids` field defines the VPC subnets into which the Fargate pods will be launched. Overmind links the profile to every subnet listed, helping you trace network reachability and security-group inheritance for the pods that will run under this profile. diff --git a/docs.overmind.tech/docs/sources/aws/Types/eks-nodegroup.md b/docs.overmind.tech/docs/sources/aws/Types/eks-nodegroup.md new file mode 100644 index 00000000..85b58201 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/eks-nodegroup.md @@ -0,0 +1,38 @@ +--- +title: EKS Nodegroup +sidebar_label: eks-nodegroup +--- + +Amazon EKS managed node groups are a higher-level abstraction that simplifies the provision and lifecycle management of the worker nodes that run your Kubernetes pods. Instead of creating and operating the underlying Amazon EC2 instances yourself, you declare the desired configuration (instance types, scaling parameters, AMI, etc.) and EKS creates and manages an Auto Scaling group on your behalf. See the official AWS documentation for full details: https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html + +**Terrafrom Mappings:** + +- `aws_eks_node_group.id` + +## Supported Methods + +- `GET`: Get a node group by unique name (`{clusterName}:{NodegroupName}`) +- ~~`LIST`~~ +- `SEARCH`: Search for node groups by cluster name + +## Possible Links + +### [`ec2-key-pair`](/sources/aws/Types/ec2-key-pair) + +If “remote access” is enabled, a node group references an EC2 key pair to allow SSH access to the worker nodes. This creates a dependency on the specified key pair. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Each node group attaches one or more security groups to the network interfaces of its nodes. These security groups control inbound and outbound traffic to the worker nodes. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +When you create a node group you must provide a list of subnets where the nodes will be launched. The node group therefore depends on, and is constrained by, the networking configuration of those subnets. + +### [`autoscaling-auto-scaling-group`](/sources/aws/Types/autoscaling-auto-scaling-group) + +Behind the scenes, a managed node group is realised as an Auto Scaling group. Changes to the node group propagate directly to its underlying Auto Scaling group. + +### [`ec2-launch-template`](/sources/aws/Types/ec2-launch-template) + +You can optionally supply a custom launch template to define advanced EC2 settings (user data, tags, block-device mappings, etc.) for the nodes. When used, the node group links to that launch template. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elb-instance-health.md b/docs.overmind.tech/docs/sources/aws/Types/elb-instance-health.md new file mode 100644 index 00000000..713545df --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elb-instance-health.md @@ -0,0 +1,19 @@ +--- +title: ELB Instance Health +sidebar_label: elb-instance-health +--- + +An ELB Instance Health resource represents the current health status of an individual Amazon EC2 instance as reported by an Elastic Load Balancer. The data is returned by the `DescribeInstanceHealth` API call and indicates whether the instance is `InService`, `OutOfService`, or in a transitional state (e.g. `Draining`, `Unknown`). By tracking these objects Overmind can warn you when a deployment will place traffic on unhealthy instances or reduce overall service capacity. +For full details see the AWS documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-healthchecks.html + +## Supported Methods + +- `GET`: Get instance health by ID (`{LoadBalancerName}/{InstanceId}`) +- `LIST`: List all instance healths +- ~~`SEARCH`~~ + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +Each ELB Instance Health object is intrinsically linked to the EC2 instance whose state it describes. Following this link allows you to inspect configuration details (such as security groups or attached volumes) that may be contributing to an unhealthy status. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elb-load-balancer.md b/docs.overmind.tech/docs/sources/aws/Types/elb-load-balancer.md new file mode 100644 index 00000000..d32502e5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elb-load-balancer.md @@ -0,0 +1,47 @@ +--- +title: Classic Load Balancer +sidebar_label: elb-load-balancer +--- + +A Classic Load Balancer (CLB) is the original generation of AWS Elastic Load Balancing. It automatically distributes incoming application or network traffic across multiple Amazon EC2 instances that are located in one or more Availability Zones, improving fault-tolerance and scalability. A CLB provides either HTTP/HTTPS or TCP load balancing and exposes a single DNS end-point that clients connect to. +Official documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/introduction.html + +**Terrafrom Mappings:** + +- `aws_elb.arn` + +## Supported Methods + +- `GET`: Get a classic load balancer by name +- `LIST`: List all classic load balancers +- `SEARCH`: Search for classic load balancers by ARN + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +The load balancer’s endpoint is presented as a DNS A/AAAA/CNAME record (e.g. `my-clb-123456.eu-west-2.elb.amazonaws.com`). Overmind links the CLB to this DNS record so that you can see which hostname is exposed publicly. + +### [`route53-hosted-zone`](/sources/aws/Types/route53-hosted-zone) + +AWS hosts the CLB DNS name inside an Amazon-owned Route 53 hosted zone, and you may also create alias or CNAME records in your own hosted zones that point to the CLB. The link shows every hosted zone that contains records referencing the load balancer. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A Classic Load Balancer must be attached to one or more subnets in each Availability Zone where it is enabled. This link reveals the exact subnets the CLB is deployed into. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Because the selected subnets belong to a specific VPC, the CLB itself resides inside that VPC. The link allows you to trace the load balancer back to its enclosing network boundary. + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +Backend EC2 instances are registered with the CLB as targets. Overmind lists every registered instance so you can assess what workloads will receive traffic from the load balancer. + +### [`elb-instance-health`](/sources/aws/Types/elb-instance-health) + +For each registered EC2 instance AWS maintains per-target health information (healthy, unhealthy, etc.). This link surfaces those health objects, letting you understand why particular instances may not be receiving traffic. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +In a VPC, a Classic Load Balancer is associated with one or more security groups that govern allowed inbound and outbound traffic. Overmind links to these security groups so you can inspect the firewall rules that protect the load balancer. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-listener.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-listener.md new file mode 100644 index 00000000..eaf7991d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-listener.md @@ -0,0 +1,36 @@ +--- +title: ELB Listener +sidebar_label: elbv2-listener +--- + +An Elastic Load Balancing (ELB) v2 Listener is the component of an Application Load Balancer (ALB) or Network Load Balancer (NLB) that checks for connection requests, using a specified protocol and port, and then routes those requests to one or more target groups according to its rules. Each listener belongs to a single load balancer, can have one default action and multiple conditional rules, and is the entry point for traffic into your load-balancing configuration. +Further details can be found in the AWS documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html + +**Terrafrom Mappings:** + +- `aws_alb_listener.arn` +- `aws_lb_listener.arn` + +## Supported Methods + +- `GET`: Get an ELB listener by ARN +- ~~`LIST`~~ +- `SEARCH`: Search for ELB listeners by load balancer ARN + +## Possible Links + +### [`elbv2-load-balancer`](/sources/aws/Types/elbv2-load-balancer) + +The listener is directly attached to exactly one load balancer. Overmind uses this link to show which ALB or NLB will be affected if the listener configuration is changed or deleted. + +### [`elbv2-rule`](/sources/aws/Types/elbv2-rule) + +A listener owns a set of rules that determine how incoming requests are evaluated and forwarded. This link exposes those rules so you can trace the impact of modifying conditions, priorities, or actions. + +### [`http`](/sources/stdlib/Types/http) + +If the listener uses the HTTP or HTTPS protocol, Overmind represents the public-facing endpoint as an `http` item. This allows cross-checking of listener ports with accessible URLs and aids in identifying unintended exposure. + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +Listener actions forward traffic to one or more target groups. Overmind links these dependencies so you can see which instances, containers, or IPs will receive traffic, helping you assess downstream blast radius. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-load-balancer.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-load-balancer.md new file mode 100644 index 00000000..a60dc818 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-load-balancer.md @@ -0,0 +1,55 @@ +--- +title: Elastic Load Balancer +sidebar_label: elbv2-load-balancer +--- + +Elastic Load Balancers distribute incoming traffic across multiple targets, improving the availability and scalability of applications. The “v2” API covers Application, Network and Gateway Load Balancers, each of which can automatically scale to meet demand and provide a single DNS endpoint for users. Full service behaviour and limits are documented in the AWS Elastic Load Balancing User Guide (https://docs.aws.amazon.com/elasticloadbalancing/latest/userguide/). + +**Terrafrom Mappings:** + +- `aws_lb.arn` +- `aws_lb.id` + +## Supported Methods + +- `GET`: Get an ELB by name +- `LIST`: List all ELBs +- `SEARCH`: Search for ELBs by ARN + +## Possible Links + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +The load balancer forwards requests to one or more target groups; each listener rule references a target group that contains the actual EC2 instances, IPs or Lambda functions receiving traffic. + +### [`elbv2-listener`](/sources/aws/Types/elbv2-listener) + +Listeners define the port and protocol that the load balancer accepts and contain the rules that map traffic to target groups; every load balancer has at least one listener. + +### [`dns`](/sources/stdlib/Types/dns) + +ELBs are accessed via a DNS name (e.g., `my-alb-123456.eu-west-1.elb.amazonaws.com`). External DNS records resolve to the IPs managed by AWS behind this name. + +### [`route53-hosted-zone`](/sources/aws/Types/route53-hosted-zone) + +Route 53 alias or CNAME records are commonly created in a hosted zone to point a friendly domain name to the load balancer’s DNS name. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +An ELB is deployed inside a specific VPC, inheriting its network boundaries and able to route traffic only within that VPC (except for internet-facing endpoints). + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +The load balancer is placed into one or more subnets; for high availability at least two subnets (usually across AZs) are required. + +### [`ec2-address`](/sources/aws/Types/ec2-address) + +Network Load Balancers can be allocated static Elastic IP addresses, one per subnet, providing fixed public IPs for the load balancer. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Each Elastic IP (for NLB) or the dynamically allocated addresses (for ALB/Gateway LB) represent the underlying IP resources that the DNS name resolves to. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Application and Gateway Load Balancers are associated with security groups which control the allowed inbound and outbound traffic to the load balancer endpoints. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-rule.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-rule.md new file mode 100644 index 00000000..d4a0f500 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-rule.md @@ -0,0 +1,17 @@ +--- +title: ELB Rule +sidebar_label: elbv2-rule +--- + +An ELBv2 listener rule specifies how an Application Load Balancer (ALB) or Network Load Balancer (NLB) should handle requests that arrive on a particular listener. Each rule has a priority, a set of conditions (for example, host-based or path-based matches) and a set of actions (such as forwarding to a target group, redirecting, or returning a fixed response). When traffic reaches the listener, the load balancer evaluates its rules in priority order and executes the actions associated with the first rule whose conditions are met. Refer to the official AWS documentation for further information: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#listener-rules + +**Terrafrom Mappings:** + +- `aws_alb_listener_rule.arn` +- `aws_lb_listener_rule.arn` + +## Supported Methods + +- `GET`: Get a rule by ARN +- ~~`LIST`~~ +- `SEARCH`: Search for rules by listener ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-group.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-group.md new file mode 100644 index 00000000..789a5692 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-group.md @@ -0,0 +1,32 @@ +--- +title: Target Group +sidebar_label: elbv2-target-group +--- + +An Amazon Elastic Load Balancing v2 (ELBv2) target group is a logical grouping of targets—such as EC2 instances, IP addresses, Lambda functions or Application Load Balancers—that a load balancer routes traffic to. It contains configuration such as the protocol and port to use, health-check settings, stickiness, deregistration delay and slow-start settings, all within a single VPC. Listeners on an Application Load Balancer (ALB) or Network Load Balancer (NLB) forward requests to one or more target groups based on listener rules. +For full details see the official AWS documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html + +**Terrafrom Mappings:** + +- `aws_alb_target_group.arn` +- `aws_lb_target_group.arn` + +## Supported Methods + +- `GET`: Get a target group by name +- `LIST`: List all target groups +- `SEARCH`: Search for target groups by load balancer ARN or target group ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +A target group is always created within a specific VPC, and all of its registered IP addresses or instance-based targets must reside in that VPC. Therefore the target group is linked to the VPC where its network resources live. + +### [`elbv2-load-balancer`](/sources/aws/Types/elbv2-load-balancer) + +Load balancers reference target groups in their listener rules. This link shows which load balancers are configured to forward traffic to the target group, or conversely, which target groups a given load balancer depends upon. + +### [`elbv2-target-health`](/sources/aws/Types/elbv2-target-health) + +Each target group has a corresponding set of target-health descriptions indicating the current health status of every registered target. This link surfaces those health objects so you can see whether the targets in the group are healthy, unhealthy, initialising or unused. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-health.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-health.md new file mode 100644 index 00000000..45e3ae77 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-health.md @@ -0,0 +1,33 @@ +--- +title: ELB Target Health +sidebar_label: elbv2-target-health +--- + +Elastic Load Balancing (v2) distributes traffic across multiple targets such as EC2 instances, IP addresses, and Lambda functions. +The ELB Target Health resource in Overmind represents the status of a single target as returned by the AWS `DescribeTargetHealth` API. +It shows whether the target is healthy, unhealthy, initialising, or draining, together with any failure reasons, so you can spot issues before a change is deployed. +Official documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeTargetHealth.html + +## Supported Methods + +- `GET`: Get target health by unique ID (`{TargetGroupArn}|{Id}|{AvailabilityZone}|{Port}`) +- ~~`LIST`~~ +- `SEARCH`: Search for target health by target group ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +When the target group’s type is `instance`, each registered EC2 instance appears as an ELB target. The target-health record shows whether that particular EC2 instance is currently considered healthy by the load balancer. + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +For target groups of type `lambda`, the Lambda function itself is the target. The target-health item reports the invocation health of the function as assessed by the load balancer. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +If the target group is of type `ip`, every registered IP address becomes a target. The target-health entry records the health of that IP address, enabling you to see whether traffic will be routed to it. + +### [`elbv2-load-balancer`](/sources/aws/Types/elbv2-load-balancer) + +The load balancer associated with the target group uses these health results to decide where to send traffic. Linking to the load balancer lets you trace how a target’s health status could affect overall load-balancer behaviour. diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-group.md b/docs.overmind.tech/docs/sources/aws/Types/iam-group.md new file mode 100644 index 00000000..d3500c2e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-group.md @@ -0,0 +1,16 @@ +--- +title: IAM Group +sidebar_label: iam-group +--- + +An IAM (Identity and Access Management) group is a logical collection of IAM users within an AWS account. Permissions—attached to the group via policies—apply to every user who is a member, making it easier to manage access at scale. Because groups do not have their own security credentials, they cannot be used to log in directly; instead, they serve solely as a mechanism for permission inheritance and simplified administration. For full details, refer to the AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_groups.html + +**Terrafrom Mappings:** + +- `aws_iam_group.arn` + +## Supported Methods + +- `GET`: Get a group by name +- `LIST`: List all IAM groups +- `SEARCH`: Search for a group by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-instance-profile.md b/docs.overmind.tech/docs/sources/aws/Types/iam-instance-profile.md new file mode 100644 index 00000000..f261d8dc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-instance-profile.md @@ -0,0 +1,26 @@ +--- +title: IAM Instance Profile +sidebar_label: iam-instance-profile +--- + +An IAM Instance Profile is a logical container for an IAM role that you can attach to an Amazon EC2 instance when it is launched. The profile passes the role’s credentials to the instance so that applications running on the instance can securely call other AWS services without embedding long-lived access keys in the code or configuration. For full details see the AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html + +**Terrafrom Mappings:** + +- `aws_iam_instance_profile.arn` + +## Supported Methods + +- `GET`: Get an IAM instance profile by name +- `LIST`: List all IAM instance profiles +- `SEARCH`: Search IAM instance profiles by ARN + +## Possible Links + +### [`iam-role`](/sources/aws/Types/iam-role) + +Every instance profile contains exactly one IAM role (though a role can exist without an instance profile). Overmind links the profile to the role it encapsulates so that you can see which permissions will be passed to the EC2 instance. + +### [`iam-policy`](/sources/aws/Types/iam-policy) + +Policies are not attached directly to the instance profile but to the role inside it. Overmind surfaces these indirect relationships so that you can trace what policies – and therefore permissions – will ultimately be available on the instance through the profile. diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-policy.md b/docs.overmind.tech/docs/sources/aws/Types/iam-policy.md new file mode 100644 index 00000000..9a122311 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-policy.md @@ -0,0 +1,33 @@ +--- +title: IAM Policy +sidebar_label: iam-policy +--- + +An IAM policy is a standalone document that defines a set of permissions which determine whether a principal (user, group, or role) is allowed or denied the ability to call specific AWS APIs. Policies are expressed in JSON, may be created and managed by customers or AWS, and are attached to identities or resources to enforce least-privilege access. See the official AWS documentation for full details: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html + +**Terrafrom Mappings:** + +- `aws_iam_policy.arn` +- `aws_iam_user_policy_attachment.policy_arn` + +## Supported Methods + +## Supported Methods + +- `GET`: Get a policy by ARN or path. `{path}` is extracted from the ARN path component. +- `LIST`: List all policies +- `SEARCH`: Search for IAM policies by ARN + +## Possible Links + +### [`iam-group`](/sources/aws/Types/iam-group) + +An IAM policy can be attached to an IAM group to grant all members of the group the permissions described in the policy. Overmind therefore links a policy to any groups to which it is attached. + +### [`iam-user`](/sources/aws/Types/iam-user) + +An IAM policy may be directly attached to an individual IAM user, granting that user the specified permissions. Overmind surfaces this relationship so you can see every user that inherits rights from the policy. + +### [`iam-role`](/sources/aws/Types/iam-role) + +IAM roles often receive permissions through attached policies. Overmind links a policy to any roles that reference it, allowing you to trace which compute workloads or federated identities can exercise the policy’s privileges. diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-role.md b/docs.overmind.tech/docs/sources/aws/Types/iam-role.md new file mode 100644 index 00000000..e4dbad1d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-role.md @@ -0,0 +1,23 @@ +--- +title: IAM Role +sidebar_label: iam-role +--- + +An AWS Identity and Access Management (IAM) role is an identity that you can assume to obtain temporary security credentials so that you can make AWS requests. Unlike users, roles do not have long-term credentials; instead, they rely on trust relationships and attached policies to define who can assume the role and what they can do once they have it. IAM roles are typically used for granting permissions to AWS services, cross-account access, or federated users. +For full details, see the AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html + +**Terrafrom Mappings:** + +- `aws_iam_role.arn` + +## Supported Methods + +- `GET`: Get an IAM role by name +- `LIST`: List all IAM roles +- `SEARCH`: Search for IAM roles by ARN + +## Possible Links + +### [`iam-policy`](/sources/aws/Types/iam-policy) + +An IAM role is functionally useless without one or more IAM policies attached to it. Overmind links an `iam-role` to the `iam-policy` resources that 1) are attached as inline or managed policies granting permissions, and 2) define the trust relationship (the role’s assume-role policy). This allows you to trace which permissions the role grants and who or what is allowed to assume it. diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-user.md b/docs.overmind.tech/docs/sources/aws/Types/iam-user.md new file mode 100644 index 00000000..d34c4c60 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-user.md @@ -0,0 +1,23 @@ +--- +title: IAM User +sidebar_label: iam-user +--- + +An IAM user is a discrete identity within AWS Identity and Access Management that represents a human, service or application which needs to interact with AWS resources. Each user has its own credentials and permissions that determine what actions it can perform in an AWS account. For full details, refer to the AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html + +**Terrafrom Mappings:** + +- `aws_iam_user.arn` +- `aws_iam_user_group_membership.user` + +## Supported Methods + +- `GET`: Get an IAM user by name +- `LIST`: List all IAM users +- `SEARCH`: Search for IAM users by ARN + +## Possible Links + +### [`iam-group`](/sources/aws/Types/iam-group) + +IAM users can be members of one or more IAM groups, inheriting the group’s managed and inline policies. Overmind therefore links an IAM user to the `iam-group` type whenever the user is listed as a member of that group. diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-alias.md b/docs.overmind.tech/docs/sources/aws/Types/kms-alias.md new file mode 100644 index 00000000..51dd4e6b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-alias.md @@ -0,0 +1,23 @@ +--- +title: KMS Alias +sidebar_label: kms-alias +--- + +An AWS Key Management Service (KMS) alias is a human-readable pointer to a specific KMS key, allowing you to reference that key without exposing its full KeyID or ARN. Aliases make it simpler to rotate keys and update applications, because you can move the alias to a new key rather than changing code or configurations that use the key directly. They are unique within an account and region, and can reference either customer-managed or AWS-managed keys. +For further details, see the official AWS documentation: https://docs.aws.amazon.com/kms/latest/developerguide/kms-alias.html + +**Terrafrom Mappings:** + +- `aws_kms_alias.arn` + +## Supported Methods + +- `GET`: Get an alias by keyID/aliasName +- `LIST`: List all aliases +- `SEARCH`: Search aliases by keyID + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +Each alias is a shorthand reference that maps to exactly one KMS key; the link shows which underlying `kms-key` the alias currently points to, enabling you to trace risk and usage back to the actual cryptographic key material. diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-custom-key-store.md b/docs.overmind.tech/docs/sources/aws/Types/kms-custom-key-store.md new file mode 100644 index 00000000..edb3cb89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-custom-key-store.md @@ -0,0 +1,16 @@ +--- +title: Custom Key Store +sidebar_label: kms-custom-key-store +--- + +A custom key store in AWS Key Management Service (KMS) enables you to back your KMS keys with your own AWS CloudHSM cluster rather than with the default, multi-tenant KMS hardware security modules. This gives you exclusive control over the cryptographic hardware that protects your key material while still allowing you to use KMS APIs and integrations. You can create, connect, disconnect, or delete a custom key store, and any KMS keys that reside in it remain under your sole tenancy. See the official AWS documentation for full details: https://docs.aws.amazon.com/kms/latest/developerguide/custom-key-store-overview.html + +**Terrafrom Mappings:** + +- `aws_kms_custom_key_store.id` + +## Supported Methods + +- `GET`: Get a custom key store by its ID +- `LIST`: List all custom key stores +- `SEARCH`: Search custom key store by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-grant.md b/docs.overmind.tech/docs/sources/aws/Types/kms-grant.md new file mode 100644 index 00000000..63c2197a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-grant.md @@ -0,0 +1,30 @@ +--- +title: KMS Grant +sidebar_label: kms-grant +--- + +AWS Key Management Service (KMS) grants are lightweight authorisations that give a specified principal permission to use a particular KMS key for a defined set of operations (such as Encrypt, Decrypt, GenerateDataKey or RetireGrant). Unlike key policies and IAM policies, grants can be created and retired programmatically and have an optional time-to-live, making them ideal for short-lived workloads or delegated access. For a full description see the official AWS documentation: https://docs.aws.amazon.com/kms/latest/developerguide/grants.html + +**Terrafrom Mappings:** + +- `aws_kms_grant.grant_id` + +## Supported Methods + +- `GET`: Get a grant by keyID/grantId +- ~~`LIST`~~ +- `SEARCH`: Search grants by keyID + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +Every grant is created against exactly one KMS key. The grant specifies which operations are allowed on that key, so the relationship is “KMS key ­— has → grant”. + +### [`iam-user`](/sources/aws/Types/iam-user) + +An IAM user can appear as the grantee principal or the retiring principal in a grant. If the user is referenced, the link shows which grants give that user access to which keys. + +### [`iam-role`](/sources/aws/Types/iam-role) + +Similar to IAM users, an IAM role may be listed as the grantee or retiring principal. The link reveals the grants that permit the role to use or retire access to specific KMS keys. diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-key-policy.md b/docs.overmind.tech/docs/sources/aws/Types/kms-key-policy.md new file mode 100644 index 00000000..ff75bb7f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-key-policy.md @@ -0,0 +1,23 @@ +--- +title: KMS Key Policy +sidebar_label: kms-key-policy +--- + +AWS Key Management Service (KMS) key policies are the primary access-control mechanism for customer-managed KMS keys. A key policy is a JSON document attached directly to a KMS key that defines which principals can use the key and what cryptographic operations they may perform. Every customer-managed key must have exactly one key policy, and this policy is evaluated in combination with IAM policies to determine effective permissions. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html + +**Terrafrom Mappings:** + +- `aws_kms_key_policy.key_id` + +## Supported Methods + +- `GET`: Get a KMS key policy by its Key ID +- ~~`LIST`~~ +- `SEARCH`: Search KMS key policies by Key ID + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +A KMS key policy is attached to exactly one KMS key; this link represents that one-to-one relationship. Following the link from a policy to its `kms-key` will show the cryptographic key whose usage and management are governed by the policy. diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-key.md b/docs.overmind.tech/docs/sources/aws/Types/kms-key.md new file mode 100644 index 00000000..df127d19 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-key.md @@ -0,0 +1,31 @@ +--- +title: KMS Key +sidebar_label: kms-key +--- + +An AWS Key Management Service (KMS) Key is a logical representation of a cryptographic key used to encrypt and decrypt data across AWS services and your own applications. Each key is uniquely identifiable by its Key ID and Amazon Resource Name (ARN), can be either customer-managed or AWS-managed, and is stored within an AWS-managed hardware security module (HSM) cluster or, when using a custom key store, in an AWS CloudHSM cluster that you control. KMS Keys are central to implementing envelope encryption, controlling access to encrypted resources, and meeting compliance requirements related to data protection. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html + +**Terrafrom Mappings:** + +- `aws_kms_key.key_id` + +## Supported Methods + +- `GET`: Get a KMS Key by its ID +- `LIST`: List all KMS Keys +- `SEARCH`: Search for KMS Keys by ARN + +## Possible Links + +### [`kms-custom-key-store`](/sources/aws/Types/kms-custom-key-store) + +A KMS Key may reside in a custom key store backed by your own AWS CloudHSM cluster. This link is produced when the key’s `KeyStoreId` attribute is set, allowing Overmind to trace the relationship between the key and the custom key store that physically holds its material. + +### [`kms-key-policy`](/sources/aws/Types/kms-key-policy) + +Every KMS Key has exactly one key policy that defines which principals are authorised to use or administer the key. Overmind links a key to its policy so that you can quickly inspect who can access the key and identify potential misconfigurations or excessive permissions. + +### [`kms-grant`](/sources/aws/Types/kms-grant) + +Grants provide time-bound or scoped permissions for principals to use a KMS Key without modifying its key policy. Overmind records links from a key to all active grants, enabling you to see what temporary or delegated access exists and to assess the risk of unintended key usage. diff --git a/docs.overmind.tech/docs/sources/aws/Types/lambda-event-source-mapping.md b/docs.overmind.tech/docs/sources/aws/Types/lambda-event-source-mapping.md new file mode 100644 index 00000000..8696a046 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/lambda-event-source-mapping.md @@ -0,0 +1,35 @@ +--- +title: Lambda Event Source Mapping +sidebar_label: lambda-event-source-mapping +--- + +AWS Lambda event source mappings are configuration objects that connect an event-producing resource (for example, an SQS queue, DynamoDB stream, Kinesis data stream or Amazon MQ broker) to a Lambda function. They tell Lambda from which resource to poll, what batch size to use, whether to enable the mapping immediately, and numerous advanced options such as filtering and batching windows. In essence, an event source mapping is the glue that turns an upstream stream or queue into invocations of your function. +Official documentation: https://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html#event-source-mapping + +**Terrafrom Mappings:** + +- `aws_lambda_event_source_mapping.arn` + +## Supported Methods + +- `GET`: Get a Lambda event source mapping by UUID +- `LIST`: List all Lambda event source mappings +- `SEARCH`: Search for Lambda event source mappings by Event Source ARN (SQS, DynamoDB, Kinesis, etc.) + +## Possible Links + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +Every event source mapping targets exactly one Lambda function. The mapping’s `FunctionName` points to the ARN of that function, so Overmind will create a link from the mapping to the lambda-function resource it invokes. + +### [`dynamodb-table`](/sources/aws/Types/dynamodb-table) + +When the event source ARN refers to a DynamoDB stream, the underlying DynamoDB table is important context. Overmind links the mapping to the dynamodb-table that owns the stream so that you can trace how table updates lead to Lambda executions. + +### [`sqs-queue`](/sources/aws/Types/sqs-queue) + +For SQS, the mapping’s `EventSourceArn` is the ARN of an SQS queue. Linking to the sqs-queue resource lets you understand queue configuration (visibility timeout, encryption, redrive policy) and how it might influence Lambda processing. + +### [`rds-db-cluster`](/sources/aws/Types/rds-db-cluster) + +If the event source is an Amazon RDS for PostgreSQL or MySQL DB cluster emitting events through Amazon RDS for PostgreSQL logical replication slots (via the `RDS Data API` or Aurora’s `MysqlBinlog` integration), the mapping may reference the cluster’s ARN. Overmind links to the rds-db-cluster so you can assess the impact of database changes on the Lambda workflow. diff --git a/docs.overmind.tech/docs/sources/aws/Types/lambda-function.md b/docs.overmind.tech/docs/sources/aws/Types/lambda-function.md new file mode 100644 index 00000000..4352b000 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/lambda-function.md @@ -0,0 +1,44 @@ +--- +title: Lambda Function +sidebar_label: lambda-function +--- + +AWS Lambda is a serverless compute service that runs your code in response to events and automatically manages the underlying compute resources for you. A Lambda function is the fundamental execution unit: it contains your application code, runtime settings and configuration such as memory, timeout and environment variables. For a full description see the official AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/welcome.html + +**Terrafrom Mappings:** + +- `aws_lambda_function.arn` +- `aws_lambda_function_event_invoke_config.id` +- `aws_lambda_function_url.function_arn` + +## Supported Methods + +- `GET`: Get a lambda function by name +- `LIST`: List all lambda functions +- `SEARCH`: Search for lambda functions by ARN + +## Possible Links + +### [`iam-role`](/sources/aws/Types/iam-role) + +Each Lambda function is executed with an IAM role (its “execution role”). Overmind links the function to that `iam-role` so you can immediately see what permissions the function has and what downstream resources could be affected by its actions. + +### [`s3-bucket`](/sources/aws/Types/s3-bucket) + +A Lambda function can be triggered by S3 events (e.g. object creation) or load its deployment artefact from an S3 bucket. Overmind links the function to any referenced `s3-bucket` so you can assess event-driven couplings and code-package storage risks. + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +Lambda functions may subscribe to, or publish messages to, Amazon SNS topics. When a function is configured as an SNS subscription target, Overmind links it to the relevant `sns-topic` so that you can trace message flows and understand failure blast-radius. + +### [`sqs-queue`](/sources/aws/Types/sqs-queue) + +Lambda can poll SQS queues as an event source. Overmind establishes a link between the function and the `sqs-queue` it consumes so that queue backlogs, permissions and dead-letter configurations are visible in the dependency graph. + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +A Lambda function can synchronously or asynchronously invoke another Lambda function (for example, in micro-service fan-out patterns). Overmind links calling and called `lambda-function` resources to expose these internal service dependencies. + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +Application Load Balancers (ALB) can forward requests to Lambda targets via an ELBv2 target group. Overmind links the function to any associated `elbv2-target-group`, allowing you to see inbound HTTP pathways and evaluate scaling or security implications. diff --git a/docs.overmind.tech/docs/sources/aws/Types/lambda-layer-version.md b/docs.overmind.tech/docs/sources/aws/Types/lambda-layer-version.md new file mode 100644 index 00000000..fdcf79b1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/lambda-layer-version.md @@ -0,0 +1,17 @@ +--- +title: Lambda Layer Version +sidebar_label: lambda-layer-version +--- + +AWS Lambda Layer Version represents an immutable, version-numbered snapshot of a Lambda layer—an archive of shared code, libraries, custom runtimes or other assets that can be attached to multiple Lambda functions. Each time you publish a layer you create a new layer version, referenced in the form `arn:aws:lambda:::layer::`. Using layers helps decouple shared dependencies from individual function packages, streamline updates and encourage code reuse across your serverless estate. +Further details can be found in the official AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html + +**Terrafrom Mappings:** + +- `aws_lambda_layer_version.arn` + +## Supported Methods + +- `GET`: Get a layer version by full name (`{layerName}:{versionNumber}`) +- ~~`LIST`~~ +- `SEARCH`: Search for layer versions by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/lambda-layer.md b/docs.overmind.tech/docs/sources/aws/Types/lambda-layer.md new file mode 100644 index 00000000..1641736c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/lambda-layer.md @@ -0,0 +1,19 @@ +--- +title: Lambda Layer +sidebar_label: lambda-layer +--- + +AWS Lambda Layers are a packaging construct used to share code, data, and runtimes between multiple Lambda functions. A layer is published once and can then be referenced by any function in the same AWS account (or, if shared, by functions in other accounts), keeping deployment packages small and ensuring that common dependencies are managed in a single place. Overmind surfaces Lambda Layers so that you can see which functions depend on them and understand the blast radius of any proposed change. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html + +## Supported Methods + +- ~~`GET`~~ +- `LIST`: List all lambda layers +- ~~`SEARCH`~~ + +## Possible Links + +### [`lambda-layer-version`](/sources/aws/Types/lambda-layer-version) + +A Lambda Layer can have multiple immutable versions; this link shows the individual versions that belong to the parent layer. diff --git a/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall-policy.md b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall-policy.md new file mode 100644 index 00000000..bea09a33 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall-policy.md @@ -0,0 +1,31 @@ +--- +title: Network Firewall Policy +sidebar_label: network-firewall-firewall-policy +--- + +An AWS Network Firewall Policy is the central configuration object that tells the AWS Network Firewall service how to inspect, filter, and log traffic that flows through a firewall. The policy groups together references to stateless and stateful rule groups, sets default actions for traffic that does not match a rule, and can optionally attach TLS inspection configurations. Multiple firewalls can share the same policy, making it easy to apply a consistent security posture across different VPCs or accounts. +For full service documentation, see the official AWS docs: https://docs.aws.amazon.com/network-firewall/latest/developerguide/firewall-policies.html + +**Terrafrom Mappings:** + +- `aws_networkfirewall_firewall_policy.name` + +## Supported Methods + +- `GET`: Get a Network Firewall Policy by name +- `LIST`: List Network Firewall Policies +- `SEARCH`: Search for Network Firewall Policies by ARN + +## Possible Links + +### [`network-firewall-rule-group`](/sources/aws/Types/network-firewall-rule-group) + +A firewall policy is essentially a collection of references to stateless and stateful rule groups. Each rule group defined under the policy dictates how specific traffic patterns are handled. Overmind links a policy to its rule groups so that you can quickly understand which inspection rules are being applied. + +### [`network-firewall-tls-inspection-configuration`](/sources/aws/Types/network-firewall-tls-inspection-configuration) + +If the policy includes a TLS inspection configuration, encrypted traffic can be decrypted, inspected, and then re-encrypted. Overmind links the policy to any associated TLS inspection configurations to show whether the firewall is capable of deep packet inspection for TLS flows. + +### [`kms-key`](/sources/aws/Types/kms-key) + +Firewall policies may specify a KMS key for the encryption of log data or stateful rule group data at rest. Overmind surfaces this link so that you can assess the cryptographic controls protecting your firewall’s sensitive data. diff --git a/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall.md b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall.md new file mode 100644 index 00000000..f185d8a0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall.md @@ -0,0 +1,42 @@ +--- +title: Network Firewall +sidebar_label: network-firewall-firewall +--- + +AWS Network Firewall is a managed, stateful, layer-4 and layer-7 firewall service that you deploy inside your own Amazon Virtual Private Cloud (VPC). It lets you inspect and filter both inbound and outbound traffic by applying rule groups that you author or obtain from third-party providers. Because the service is fully managed, AWS handles availability, scaling and patching, allowing you to focus on writing network-security rules rather than on the underlying infrastructure. For a full overview, see the official documentation: https://docs.aws.amazon.com/network-firewall/latest/developerguide/what-is-aws-network-firewall.html + +**Terrafrom Mappings:** + +- `aws_networkfirewall_firewall.name` + +## Supported Methods + +- `GET`: Get a Network Firewall by name +- `LIST`: List Network Firewalls +- `SEARCH`: Search for Network Firewalls by ARN + +## Possible Links + +### [`network-firewall-firewall-policy`](/sources/aws/Types/network-firewall-firewall-policy) + +Each Network Firewall is associated with exactly one firewall policy, which defines the stateful and stateless rule groups, default actions and logging configuration that the firewall must enforce. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A firewall is deployed into one or more dedicated subnets—known as firewall subnets—within the VPC. These subnets host the firewall endpoints that inspect traffic traversing the Availability Zones. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The firewall operates inside a specific VPC, inspecting traffic that enters, leaves or moves within that VPC according to the routing configuration you set up. + +### [`s3-bucket`](/sources/aws/Types/s3-bucket) + +You can configure Network Firewall to export alert and flow logs to an Amazon S3 bucket for long-term storage, auditing or further analysis; the bucket therefore becomes a downstream logging destination for the firewall. + +### [`iam-policy`](/sources/aws/Types/iam-policy) + +Creation, modification and deletion of Network Firewall resources are controlled through IAM policies. These policies grant or deny the required `network-firewall:*` permissions to principals such as users, roles and service accounts. + +### [`kms-key`](/sources/aws/Types/kms-key) + +If you choose to encrypt log data that Network Firewall delivers to Amazon S3 or CloudWatch Logs with a customer-managed key, the firewall references an AWS KMS key. The key is used for server-side encryption of the exported log objects. diff --git a/docs.overmind.tech/docs/sources/aws/Types/network-firewall-rule-group.md b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-rule-group.md new file mode 100644 index 00000000..3de4cd07 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-rule-group.md @@ -0,0 +1,30 @@ +--- +title: Network Firewall Rule Group +sidebar_label: network-firewall-rule-group +--- + +AWS Network Firewall Rule Groups are reusable collections of stateless or stateful inspection rules that you attach to a Network Firewall policy. They let you define, version, and manage traffic-inspection logic independently from the firewalls that enforce it. A rule group may contain Suricata-compatible stateful rules, 5-tuple stateless rules, or a combination of both, and can optionally be encrypted with a customer-managed AWS KMS key. See the official AWS documentation for full details: https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-groups.html + +**Terrafrom Mappings:** + +- `aws_networkfirewall_rule_group.name` + +## Supported Methods + +- `GET`: Get a Network Firewall Rule Group by name +- `LIST`: List Network Firewall Rule Groups +- `SEARCH`: Search for Network Firewall Rule Groups by ARN + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +If the rule group was created with an `EncryptionConfiguration`, the ARN of the customer-managed KMS key used for encryption is stored in the resource metadata. Overmind therefore links the rule group to the corresponding `kms-key` item. + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +Operational teams often configure CloudWatch alarms on Network Firewall metrics that publish to an SNS topic; the alarm definition contains the rule group ARN as a dimension. When such a relationship exists, Overmind links the rule group to the `sns-topic` so that users can trace alerting pathways. + +### [`network-firewall-rule-group`](/sources/aws/Types/network-firewall-rule-group) + +Firewall policies can reference multiple rule groups, and a single rule group can be associated with several policies. Overmind records these associations, allowing one rule group to be linked to other rule groups that are attached to the same policy or that replace it through versioning. diff --git a/docs.overmind.tech/docs/sources/aws/Types/network-firewall-tls-inspection-configuration.md b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-tls-inspection-configuration.md new file mode 100644 index 00000000..f447983d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-tls-inspection-configuration.md @@ -0,0 +1,13 @@ +--- +title: Network Firewall TLS Inspection Configuration +sidebar_label: network-firewall-tls-inspection-configuration +--- + +An AWS Network Firewall TLS Inspection Configuration represents the collection of certificates and related settings that AWS Network Firewall uses to decrypt, inspect and, when appropriate, re-encrypt TLS-encrypted traffic flowing through a firewall. The configuration is referenced by a firewall policy and allows the firewall to analyse traffic that would otherwise be opaque, enabling the detection of threats hidden inside encrypted sessions. +For full details, see the AWS documentation: https://docs.aws.amazon.com/network-firewall/latest/developerguide/tls-inspection-configuration.html + +## Supported Methods + +- `GET`: Get a Network Firewall TLS Inspection Configuration by name +- `LIST`: List Network Firewall TLS Inspection Configurations +- `SEARCH`: Search for Network Firewall TLS Inspection Configurations by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-attachment.md new file mode 100644 index 00000000..e2c5a117 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-attachment.md @@ -0,0 +1,21 @@ +--- +title: Networkmanager Connect Attachment +sidebar_label: networkmanager-connect-attachment +--- + +A Network Manager Connect Attachment represents the logical connection used to link a third-party SD-WAN, on-premises router or other non-AWS network appliance to an AWS Cloud WAN core network. It enables you to extend a core network beyond AWS, transporting traffic through GRE tunnels that are established and maintained by a subsequently created Connect Peer. +For full details see the AWS documentation: https://docs.aws.amazon.com/network-manager/latest/cloudwan/cloudwan-network-attachments.html#cloudwan-attachment-connect + +**Terrafrom Mappings:** + +- `aws_networkmanager_core_network.id` + +## Supported Methods + +- `GET`: + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every Connect Attachment is created inside a specific Cloud WAN core network, referenced by its `CoreNetworkId`. Consequently, Overmind links a connect attachment back to the corresponding `networkmanager-core-network` so that you can trace how external connectivity feeds into, and potentially affects, the wider Cloud WAN topology. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer-association.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer-association.md new file mode 100644 index 00000000..92a76c89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer-association.md @@ -0,0 +1,31 @@ +--- +title: Networkmanager Connect Peer Association +sidebar_label: networkmanager-connect-peer-association +--- + +An AWS Network Manager **Connect Peer Association** records the relationship between a Transit Gateway Connect peer and the on-premises device and link through which that peer reaches the AWS global network. It lets you see which Connect peers are presently attached to which devices and links inside a particular Global Network, and in which state the attachment currently is (for example, _pending_ or _available_). +For full API details, refer to the official AWS documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_ConnectPeerAssociation.html + +## Supported Methods + +- `GET`: Get a Networkmanager Connect Peer Association +- `LIST`: List all Networkmanager Connect Peer Associations +- `SEARCH`: Search for Networkmanager ConnectPeerAssociations by GlobalNetworkId + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +The association is scoped to a single Global Network; every Connect Peer Association includes the `GlobalNetworkId` that ties it back to this parent resource. + +### [`networkmanager-connect-peer`](/sources/aws/Types/networkmanager-connect-peer) + +The association identifies the specific Connect Peer (`ConnectPeerId`) whose attachment details are being tracked. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +If the Connect peer terminates on a particular on-premises or edge device, the association includes the `DeviceId`, linking it to this device resource. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +Where applicable, the association also records the `LinkId`, showing which physical or logical link is being used by the Connect peer to reach AWS. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer.md new file mode 100644 index 00000000..8ec2f5b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer.md @@ -0,0 +1,40 @@ +--- +title: Networkmanager Connect Peer +sidebar_label: networkmanager-connect-peer +--- + +An AWS Network Manager **Connect Peer** represents one end of a GRE tunnel that is established over a Network Manager _Connect attachment_ (for example, between an AWS Transit Gateway/Cloud WAN core network and an external router). +The peer stores the tunnel’s **inside and outside IP addresses**, BGP configuration (peer ASN, BGP addresses and keys), the subnet in which the tunnel terminates, and the current operational state. Creating the peer is the final step that brings a Connect attachment into service, enabling traffic to flow between AWS and on-premises or third-party networks. +For full details see the official AWS documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_ConnectPeer.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_connect_peer.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Connect Peer by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +A Connect peer ultimately belongs to a core network; through its parent Connect attachment it is associated with a specific core network ID, so the peer can be traced back to the Cloud WAN or Transit Gateway core it serves. + +### [`networkmanager-connect-attachment`](/sources/aws/Types/networkmanager-connect-attachment) + +Each Connect peer is created **within** a single Connect attachment. This link identifies the attachment that houses the peer and through which the GRE tunnel is terminated. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The peer exposes both _inside_ and _outside_ tunnel IP addresses. These addresses are modelled as IP resources and linked so you can see which IPs are consumed by the peer. + +### [`rdap-asn`](/sources/stdlib/Types/rdap-asn) + +When BGP is enabled the peer records the remote BGP ASN. Overmind links that ASN so you can quickly inspect public registration information for the autonomous system you are peering with. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +The peer must be associated with a specific subnet that contains the tunnel’s AWS endpoint. Linking to the EC2 subnet shows the precise network segment in which the peer resides, helping to check routing and security settings. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connection.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connection.md new file mode 100644 index 00000000..f24ff668 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connection.md @@ -0,0 +1,30 @@ +--- +title: Networkmanager Connection +sidebar_label: networkmanager-connection +--- + +An AWS Network Manager Connection represents the logical relationship between two network devices (for example, a branch router and a transit gateway) inside an AWS Global Network. It stores metadata about how the two endpoints are linked, enabling Network Manager to map, monitor and troubleshoot your private WAN from a single view. See the official AWS documentation for full details: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_Connection.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_connection.arn` + +## Supported Methods + +- `GET`: Get a Networkmanager Connection +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Connections by GlobalNetworkId, Device ARN, or Connection ARN + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +Every connection is created within exactly one Global Network. Overmind follows this link to understand which overarching corporate network the connection belongs to and to enumerate all other resources that share the same scope. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +A connection is realised by one or two underlying Links, representing the actual circuits or VPN tunnels that carry traffic. Linking to these allows Overmind to surface characteristics such as bandwidth, provider and health for each side of the connection. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +Each connection terminates on two Devices (the `SourceDeviceId` and `DestinationDeviceId`). From a connection, Overmind can pivot to the involved devices to reveal their configurations, attached links and any downstream dependencies that could be affected by changes to the connection. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network-policy.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network-policy.md new file mode 100644 index 00000000..2dfff562 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network-policy.md @@ -0,0 +1,22 @@ +--- +title: Networkmanager Core Network Policy +sidebar_label: networkmanager-core-network-policy +--- + +An AWS Network Manager Core Network Policy represents the set of declarative rules that describe how traffic may flow within and between the segments of an AWS Cloud WAN core network (for example, how on-premises VPNs, VPCs and Transit Gateways are connected, and which segments are allowed to communicate). Each policy is versioned and attached to a single core network, allowing you to stage, validate and apply changes safely. For further details see the AWS documentation: https://docs.aws.amazon.com/network-manager/latest/cloudwan/cloudwan-policy-operations.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_core_network_policy.core_network_id` + +## Supported Methods + +- `GET`: Get a Networkmanager Core Network Policy by Core Network id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every core network policy is bound to exactly one core network; therefore, Overmind links a `networkmanager-core-network-policy` item back to the corresponding `networkmanager-core-network` to show which core network the policy governs and to make it easier to assess the blast-radius of changes. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network.md new file mode 100644 index 00000000..bb9905ca --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network.md @@ -0,0 +1,27 @@ +--- +title: Networkmanager Core Network +sidebar_label: networkmanager-core-network +--- + +An AWS Network Manager **core network** represents the logical, centrally-managed backbone created by AWS Cloud WAN. It defines the global routing fabric, network segments, and edge locations that connect your AWS Regions and on-premises sites. Once a core network is in place you can attach VPCs, VPNs, Direct Connects and third-party SD-WAN devices, and let Cloud WAN automatically propagate routes between them according to the policy you supply. +For further details see the [official documentation](https://docs.aws.amazon.com/vpc/latest/cloudwan/what-is-cloudwan.html). + +**Terrafrom Mappings:** + +- `aws_networkmanager_core_network.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Core Network by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network-policy`](/sources/aws/Types/networkmanager-core-network-policy) + +Every core network is governed by a **core network policy** that declares its segments, attachment permissions, and routing intent. Overmind links a `networkmanager-core-network` to its current `networkmanager-core-network-policy` so that you can inspect or diff the policy that is actively controlling the network. + +### [`networkmanager-connect-peer`](/sources/aws/Types/networkmanager-connect-peer) + +A **Connect peer** represents a GRE/BGP session that terminates on a Connect attachment belonging to a core network. Overmind exposes this link to show which Connect peers (and therefore which on-premises routers) are logically attached to the given core network. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-device.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-device.md new file mode 100644 index 00000000..b96cef18 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-device.md @@ -0,0 +1,39 @@ +--- +title: Networkmanager Device +sidebar_label: networkmanager-device +--- + +An AWS Network Manager Device represents a physical or virtual network appliance (e.g. router, firewall, SD-WAN box, software VPN endpoint) that you register with a Global Network in AWS Network Manager. Once registered, the device becomes a first-class object that can be linked to Sites, Links and Connections, allowing you to model and monitor your entire hybrid network topology in AWS. +For full details see the AWS API reference: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_Device.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_device.arn` + +## Supported Methods + +- `GET`: Get a Networkmanager Device +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Devices by GlobalNetworkId, `{GlobalNetworkId|SiteId}` or ARN + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +A device is always created inside a single Global Network. This link shows which Global Network the device belongs to so you can understand its administrative domain. + +### [`networkmanager-site`](/sources/aws/Types/networkmanager-site) + +Each device is associated with one Site (for example, a particular data centre or branch office). The link reveals the physical location context of the device. + +### [`networkmanager-link-association`](/sources/aws/Types/networkmanager-link-association) + +A device can have one or more Link Associations that describe the physical or logical circuits (Links) terminating on that device. Following this link surfaces the underlying connectivity for the device. + +### [`networkmanager-connection`](/sources/aws/Types/networkmanager-connection) + +Connections model the logical relationship between two devices. This link lists all point-to-point or multi-point Connections in which the device participates. + +### [`networkmanager-network-resource-relationship`](/sources/aws/Types/networkmanager-network-resource-relationship) + +This link captures any additional resource relationships (for example, Transit Gateway attachments or VPNs) that reference the device, providing a holistic view of dependencies and potential blast-radius. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-global-network.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-global-network.md new file mode 100644 index 00000000..af31ebf5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-global-network.md @@ -0,0 +1,51 @@ +--- +title: Network Manager Global Network +sidebar_label: networkmanager-global-network +--- + +An AWS Network Manager Global Network is the top-level container that represents your organisation’s private global network within AWS. It groups together sites, on-premises devices, AWS Transit Gateways, and the connections between them, allowing you to view and manage the entire topology from a single place. You must create a global network before you can register any resources with Network Manager. +Official documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_GlobalNetwork.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_global_network.arn` + +## Supported Methods + +- `GET`: Get a global network by id +- `LIST`: List all global networks +- `SEARCH`: Search for a global network by ARN + +## Possible Links + +### [`networkmanager-site`](/sources/aws/Types/networkmanager-site) + +A Site is created inside a Global Network. Each `networkmanager-site` record therefore links back to the Global Network that owns it. + +### [`networkmanager-transit-gateway-registration`](/sources/aws/Types/networkmanager-transit-gateway-registration) + +Transit Gateways must be registered with a specific Global Network before they can be visualised or managed by Network Manager. These registration objects reference the parent Global Network. + +### [`networkmanager-connect-peer-association`](/sources/aws/Types/networkmanager-connect-peer-association) + +A Connect Peer Association represents the attachment of a Connect peer to a Global Network. The association record points to the Global Network in which the peer is enrolled. + +### [`networkmanager-transit-gateway-connect-peer-association`](/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association) + +Similar to the above, but for Transit Gateway Connect peers. The association is made within the scope of a single Global Network. + +### [`networkmanager-network-resource-relationship`](/sources/aws/Types/networkmanager-network-resource-relationship) + +This type models relationships between any two resources (devices, links, TGWs, etc.) that are part of the same Global Network. Each relationship object is tied to the Global Network it belongs to. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +Links represent the physical or logical connections at a Site and, by extension, sit within the Global Network that the Site is part of. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +Devices (routers, switches, firewalls, etc.) are registered to Sites, and consequently to the parent Global Network. Each device record references its Global Network identifier. + +### [`networkmanager-connection`](/sources/aws/Types/networkmanager-connection) + +Connections join two Devices over one or more Links inside a Global Network. The connection object therefore includes the Global Network ID to denote its scope. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link-association.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link-association.md new file mode 100644 index 00000000..14b54762 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link-association.md @@ -0,0 +1,29 @@ +--- +title: Networkmanager LinkAssociation +sidebar_label: networkmanager-link-association +--- + +A Network Manager **Link Association** represents the attachment of a specific physical or logical network **link** (for example, a DIA, MPLS or broadband circuit) to a **device** (such as a router, firewall, SD-WAN appliance) that resides at a site in an AWS Cloud WAN / Network Manager **global network**. +Each association records which device terminates the link, the site it belongs to, bandwidth details and the operational state of that attachment. +Official AWS documentation: +https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_LinkAssociation.html + +## Supported Methods + +- `GET`: Get a Networkmanager Link Association +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Link Associations by GlobalNetworkId and DeviceId or LinkId + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +Every Link Association is scoped to exactly one Global Network; the GlobalNetworkId is part of the composite key for the association. Following this link lets you see all other resources (sites, devices, links, transit gateways, etc.) that belong to the same overarching global network. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +The association couples a device to a particular LinkId. Traversing this link shows the underlying circuit or connectivity object that is being attached, along with its provider, bandwidth and cost details. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +The DeviceId in the association identifies the hardware or virtual appliance that terminates the link. Navigating this link reveals the device’s interfaces, status and any other links or connections it participates in. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link.md new file mode 100644 index 00000000..2c44c83a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link.md @@ -0,0 +1,35 @@ +--- +title: Networkmanager Link +sidebar_label: networkmanager-link +--- + +An AWS Network Manager **Link** represents a physical or logical connection (for example, an MPLS circuit, Direct Connect connection, broadband, or internet link) that provides connectivity at a specific site within a global network. Links are used by Network Manager to calculate network health, aggregate telemetry and visualise topology. Each link is created inside a Site, and therefore inside a Global Network, and can later be associated with one or more network devices. +Official documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_Link.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_link.arn` + +## Supported Methods + +- `GET`: Get a Networkmanager Link +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Links by GlobalNetworkId, GlobalNetworkId with SiteId, or ARN + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +A Link is a component of a single Global Network; this edge points from the Link to the Global Network that owns it. + +### [`networkmanager-link-association`](/sources/aws/Types/networkmanager-link-association) + +A Link can be associated with one or more devices. These associations are represented by Network Manager Link Association resources, which reference the Link as their parent. + +### [`networkmanager-site`](/sources/aws/Types/networkmanager-site) + +Every Link resides in exactly one Site; this relationship shows which Site the Link belongs to. + +### [`networkmanager-network-resource-relationship`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Network Manager records discovered relationships between Links and other network resources (for example, AWS Transit Gateway attachments). This edge captures those discovered `network-resource-relationship` objects that involve the Link. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-network-resource-relationship.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-network-resource-relationship.md new file mode 100644 index 00000000..3c38c328 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-network-resource-relationship.md @@ -0,0 +1,43 @@ +--- +title: Networkmanager Network Resource Relationships +sidebar_label: networkmanager-network-resource-relationship +--- + +Represents an association between two AWS Network Manager resources within a single Global Network. A Network Resource Relationship records how different components—such as devices, links, connections and Direct Connect objects—are connected, enabling topology visualisation and impact analysis. Each relationship object identifies a **source resource**, a **destination resource**, and the **type of relationship** (for example `CONNECTED_TO` or `CHILD_OF`). +For full field-level details see the AWS API reference: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_NetworkResourceRelationship.html + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager NetworkResourceRelationships by GlobalNetworkId + +## Possible Links + +### [`networkmanager-connection`](/sources/aws/Types/networkmanager-connection) + +A Network Manager connection (for example a VPN or Transit Gateway attachment) can appear as either the **source** or **destination** in a relationship, indicating that it is logically connected to another resource—most commonly a site, device or Direct Connect virtual interface. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +Devices (routers, firewalls or SD-WAN appliances) are frequently linked to links and connections. When a device participates in a relationship, the record shows which link it uses or which connection terminates on the device. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +A link represents physical or logical connectivity (for example an MPLS circuit). Relationships illustrate which device, site or Direct Connect virtual interface is using, or is reached through, a given link. + +### [`networkmanager-site`](/sources/aws/Types/networkmanager-site) + +Site resources group devices and links. Relationships referencing a site capture a **CHILD_OF** type association, showing that a particular device or link belongs to, or is located within, the site. + +### [`directconnect-connection`](/sources/aws/Types/directconnect-connection) + +Direct Connect connections are mapped into the global network; relationships show how a Direct Connect line is attached to a Network Manager link or gateway, providing visibility of dedicated connectivity paths. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +When a Direct Connect gateway is part of a global network, relationships identify which connections or virtual interfaces are routed through the gateway, enabling you to trace traffic flows. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +Virtual interfaces (private, public or transit) may be related to Direct Connect connections, gateways or Network Manager links. The relationship clarifies which physical connection a VIF is presented on and how it integrates with the wider network. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site-to-site-vpn-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site-to-site-vpn-attachment.md new file mode 100644 index 00000000..5a3f8b7c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site-to-site-vpn-attachment.md @@ -0,0 +1,23 @@ +--- +title: Networkmanager Site To Site Vpn Attachment +sidebar_label: networkmanager-site-to-site-vpn-attachment +--- + +A Network Manager Site-to-Site VPN attachment represents the connection of an AWS Site-to-Site VPN to an AWS Cloud WAN / Network Manager core network. By creating this attachment you allow traffic from a remote on-premises site, carried over an IPsec VPN tunnel, to be routed through the core network alongside other AWS and on-premises connections. +Further information can be found in the [official AWS documentation](https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_SiteToSiteVpnAttachment.html). + +**Terrafrom Mappings:** + +- `aws_networkmanager_site_to_site_vpn_attachment.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Site To Site Vpn Attachment by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Each Site-to-Site VPN attachment is created inside a single core network, so the attachment item is linked to the `networkmanager-core-network` that owns it. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site.md new file mode 100644 index 00000000..b905c942 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site.md @@ -0,0 +1,30 @@ +--- +title: Networkmanager Site +sidebar_label: networkmanager-site +--- + +An AWS Network Manager **Site** represents a real-world location—such as a corporate office, data centre or colocation facility—that forms part of an organisation’s Global Network. It provides the context in which devices and network links are deployed, enabling AWS Network Manager to map physical geography to logical connectivity. For a full description of the resource and its attributes, see the official AWS documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_Site.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_site.arn` + +## Supported Methods + +- `GET`: Get a Networkmanager Site +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Sites by GlobalNetworkId or Site ARN + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +A Site is always created within a single Global Network. The `GlobalNetworkId` on the Site identifies its parent `networkmanager-global-network`, forming a one-to-many relationship (one Global Network, many Sites). + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +Links represent individual network connections (e.g., MPLS, broadband) that terminate at a Site. Each `networkmanager-link` includes the `SiteId` of the Site where the connection is installed, so multiple Links can be related to one Site. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +Devices such as routers, firewalls or SD-WAN appliances are housed at a Site. Every `networkmanager-device` records the `SiteId` where it resides, creating a one-to-many relationship between a Site and its Devices. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association.md new file mode 100644 index 00000000..f74b6ec8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association.md @@ -0,0 +1,29 @@ +--- +title: Networkmanager Transit Gateway Connect Peer Association +sidebar_label: networkmanager-transit-gateway-connect-peer-association +--- + +A Network Manager Transit Gateway Connect Peer Association represents the connection between an AWS Transit Gateway Connect peer (a GRE tunnel endpoint created as part of a Transit Gateway Connect attachment) and a site that you have modelled inside AWS Network Manager. +The object records which Global Network the peer belongs to and, optionally, which on-premises device and physical/virtual link it should be mapped to. Maintaining this mapping allows Network Manager to draw accurate topology diagrams and to include the GRE tunnel in route analytics, performance monitoring, and policy assessments. + +Official documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_TransitGatewayConnectPeerAssociation.html + +## Supported Methods + +- `GET`: Get a Networkmanager Transit Gateway Connect Peer Association by id +- `LIST`: List all Networkmanager Transit Gateway Connect Peer Associations +- `SEARCH`: Search for Networkmanager Transit Gateway Connect Peer Associations by GlobalNetworkId + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +Every Transit Gateway Connect Peer Association is scoped to a single Global Network. The `GlobalNetworkId` on the association points to the corresponding `networkmanager-global-network` item, indicating which overall corporate network the peer is part of. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +The association can specify a `DeviceId` to indicate the on-premises or edge device (for example, a customer router or firewall) that terminates the GRE tunnel. Linking to the `networkmanager-device` item shows where the peer logically lands in your topology. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +If the Connect peer is tied to a particular circuit, VLAN, or VPN link at the site, the association includes a `LinkId`. This links the peer to a `networkmanager-link` item, allowing you to trace the physical or logical connectivity that underpins the GRE tunnel. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-peering.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-peering.md new file mode 100644 index 00000000..3ef718ca --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-peering.md @@ -0,0 +1,23 @@ +--- +title: Networkmanager Transit Gateway Peering +sidebar_label: networkmanager-transit-gateway-peering +--- + +An AWS Network Manager Transit Gateway Peering represents a peering attachment between an AWS Cloud WAN _core network_ and an existing AWS Transit Gateway (TGW). Creating this peering allows traffic to flow transparently between VPCs or on-premises networks connected to the Transit Gateway and the segments that make up the Cloud WAN core network, extending the reach of both fabrics without the need for additional VPNs or direct-connect links. +For more information, see the [AWS documentation](https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_TransitGatewayPeering.html). + +**Terrafrom Mappings:** + +- `aws_networkmanager_transit_gateway_peering.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Transit Gateway Peering by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every Transit Gateway Peering is created **within** a specific Cloud WAN core network; the core network is the logical container that owns the peering attachment. Consequently, querying a `networkmanager-core-network` item allows you to enumerate or drill down to its associated Transit Gateway Peerings, and conversely, each Transit Gateway Peering stores the identifier of the core network it belongs to. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-registration.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-registration.md new file mode 100644 index 00000000..d1f19264 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-registration.md @@ -0,0 +1,18 @@ +--- +title: Networkmanager Transit Gateway Registrations +sidebar_label: networkmanager-transit-gateway-registration +--- + +A Network Manager Transit Gateway Registration represents the association of an AWS Transit Gateway with an AWS Network Manager Global Network. By registering a Transit Gateway, you enable Network Manager to map its attachments, monitor routing changes and performance, and include the gateway in your overall network topology visualisation. For more information, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/tgw/register-transit-gateway.html + +## Supported Methods + +- `GET`: Get a Networkmanager Transit Gateway Registrations +- `LIST`: List all Networkmanager Transit Gateway Registrations +- `SEARCH`: Search for Networkmanager Transit Gateway Registrations by GlobalNetworkId + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +A Transit Gateway registration is always scoped to, and therefore linked with, a single Network Manager Global Network. This link indicates the parent Global Network that owns the registration, allowing Overmind to traverse from the high-level network to the individual Transit Gateway associations. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-route-table-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-route-table-attachment.md new file mode 100644 index 00000000..779efbdd --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-route-table-attachment.md @@ -0,0 +1,27 @@ +--- +title: Networkmanager Transit Gateway Route Table Attachment +sidebar_label: networkmanager-transit-gateway-route-table-attachment +--- + +The Network Manager Transit Gateway Route Table Attachment represents the binding between an AWS Transit Gateway (TGW) route table and an AWS Cloud WAN (Network Manager Core Network) segment. Creating this attachment allows routes that exist in the TGW route table to be advertised into the Cloud WAN segment and, conversely, permits segment routes to be propagated to the TGW. In effect, it provides a controlled integration point between an existing TGW-based topology and a Cloud WAN fabric. +Official API documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_CreateTransitGatewayRouteTableAttachment.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_transit_gateway_route_table_attachment.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Transit Gateway Route Table Attachment by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every Transit Gateway Route Table Attachment is created inside a specific Core Network and targets one of its segments. Therefore, the attachment is a child resource of the Core Network and inherits its administrative domain and policy constraints. + +### [`networkmanager-transit-gateway-peering`](/sources/aws/Types/networkmanager-transit-gateway-peering) + +Before a TGW route table can be attached, a Transit Gateway Peering must exist between the TGW and the Core Network. The attachment references that peering to determine the underlay connection over which route exchange will occur. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-vpc-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-vpc-attachment.md new file mode 100644 index 00000000..cf4ac041 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-vpc-attachment.md @@ -0,0 +1,23 @@ +--- +title: Networkmanager VPC Attachment +sidebar_label: networkmanager-vpc-attachment +--- + +A Network Manager VPC attachment represents the logical link between an Amazon Virtual Private Cloud (VPC) and an AWS Cloud WAN / Network Manager **core network**. By creating an attachment you allow the sub-nets inside the VPC to participate in the global routing domain managed by Network Manager, making it possible for traffic to reach other VPCs, on-premises networks, or SD-WAN devices that are also attached to the same core network. +For a detailed explanation of the resource and its properties, see the [official AWS documentation](https://docs.aws.amazon.com/vpc/latest/cloudwan/what-is-cloudwan.html). + +**Terrafrom Mappings:** + +- `aws_networkmanager_vpc_attachment.id` + +## Supported Methods + +- `GET`: Get a Networkmanager VPC Attachment by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every VPC attachment is created inside a specific core network and inherits its routing policies. The `core_network_id` field on the attachment identifies that parent, so Overmind can follow this link to reveal the wider network fabric that the VPC will join. diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster-parameter-group.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster-parameter-group.md new file mode 100644 index 00000000..643876bf --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster-parameter-group.md @@ -0,0 +1,16 @@ +--- +title: RDS Cluster Parameter Group +sidebar_label: rds-db-cluster-parameter-group +--- + +An RDS Cluster Parameter Group is a named collection of engine configuration values that are applied to every instance within an Amazon RDS or Aurora DB cluster. By adjusting the parameters in the group you can fine-tune settings such as memory management, logging, and query optimisation, and have those settings propagated consistently across the cluster. If you do not specify a custom group when you create a cluster, AWS assigns the default engine-specific parameter group. For details, see the AWS documentation: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_WorkingWithParamGroups.html. + +**Terrafrom Mappings:** + +- `aws_rds_cluster_parameter_group.arn` + +## Supported Methods + +- `GET`: Get a parameter group by name +- `LIST`: List all RDS parameter groups +- `SEARCH`: Search for a parameter group by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster.md new file mode 100644 index 00000000..d9e128c8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster.md @@ -0,0 +1,51 @@ +--- +title: RDS Cluster +sidebar_label: rds-db-cluster +--- + +Amazon Relational Database Service (RDS) Clusters provide a managed, highly-available relational database running on multiple Availability Zones. An RDS Cluster contains one or more database instances that share storage, backups, and endpoints, and can be configured for automatic fail-over and read-scaling. Aurora MySQL and Aurora PostgreSQL engines run exclusively within clusters, while other engines (e.g. MySQL, PostgreSQL) can participate in global database topologies through cluster links. +Official documentation: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_AuroraOverview.html + +**Terrafrom Mappings:** + +- `aws_rds_cluster.cluster_identifier` + +## Supported Methods + +- `GET`: Get a cluster by identifier +- `LIST`: List all RDS clusters +- `SEARCH`: Search for a cluster by ARN + +## Possible Links + +### [`rds-db-subnet-group`](/sources/aws/Types/rds-db-subnet-group) + +Each RDS Cluster is associated with a DB subnet group that defines the set of subnets (and therefore Availability Zones) in which its instances can run. + +### [`dns`](/sources/stdlib/Types/dns) + +The cluster exposes an endpoint such as `mycluster.cluster-123456789012.eu-west-2.rds.amazonaws.com`; this hostname is represented as a DNS record linked to the cluster. + +### [`rds-db-cluster`](/sources/aws/Types/rds-db-cluster) + +Clusters can reference other clusters as replication sources or targets (e.g. in an Aurora global database), creating a dependency link between the participating RDS clusters. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Traffic to and from the cluster’s instances is controlled by one or more EC2 security groups attached to the cluster. + +### [`route53-hosted-zone`](/sources/aws/Types/route53-hosted-zone) + +Organisations often create Route 53 records (A/AAAA or CNAME) in their hosted zones to provide friendly names for the cluster endpoint, linking the hosted zone to the RDS Cluster. + +### [`kms-key`](/sources/aws/Types/kms-key) + +If storage encryption is enabled, the cluster uses a customer-managed or AWS-managed KMS key; compromising or deleting the key will render the data inaccessible. + +### [`rds-option-group`](/sources/aws/Types/rds-option-group) + +Certain engines allow additional features to be enabled via option groups (e.g. Oracle options); a cluster may reference an option group to configure those extensions. + +### [`iam-role`](/sources/aws/Types/iam-role) + +An RDS Cluster can assume IAM roles for tasks such as exporting snapshots to S3, publishing logs to CloudWatch, or accessing AWS services like Kinesis; these roles are linked resources. diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-instance.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-instance.md new file mode 100644 index 00000000..7f02149b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-instance.md @@ -0,0 +1,55 @@ +--- +title: RDS Instance +sidebar_label: rds-db-instance +--- + +Amazon Relational Database Service (RDS) DB instances are the managed compute and storage resources that run your relational database engines in AWS. An instance encapsulates the underlying virtual hardware, disk, network interfaces, and database server software that form a single, addressable database node. Full service description and behaviour are documented in the AWS RDS User Guide: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Overview.DBInstance.html + +**Terrafrom Mappings:** + +- `aws_db_instance.identifier` +- `aws_db_instance_role_association.db_instance_identifier` + +## Supported Methods + +- `GET`: Get an instance by ID +- `LIST`: List all instances +- `SEARCH`: Search for instances by ARN + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +Every RDS instance exposes an endpoint such as `mydb.abc123.eu-west-2.rds.amazonaws.com`. Overmind links the instance to the corresponding DNS record so you can trace how applications resolve and reach the database. + +### [`route53-hosted-zone`](/sources/aws/Types/route53-hosted-zone) + +The automatically-created DNS record for an RDS endpoint lives inside an AWS-managed Route 53 hosted zone, and private zones in your account may contain CNAMEs pointing to it. Overmind surfaces these zones to show where the endpoint is published and overridden. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +In a VPC, an RDS instance is attached to one or more security groups that define its inbound and outbound traffic rules. These links let you audit which networks and EC2 instances are permitted to reach the database. + +### [`rds-db-parameter-group`](/sources/aws/Types/rds-db-parameter-group) + +A DB parameter group controls engine-level configuration such as `max_connections` or `log_min_duration_statement`. Each instance references exactly one parameter group (or the default), so Overmind links them for configuration drift and compliance checks. + +### [`rds-db-subnet-group`](/sources/aws/Types/rds-db-subnet-group) + +The subnet group lists the subnets (and therefore the AZs) where the instance may be placed. Linking highlights the network reachability and resiliency zone choices for the database. + +### [`rds-db-cluster`](/sources/aws/Types/rds-db-cluster) + +For Aurora and other clustered engines, individual DB instances are members of an RDS DB cluster. Overmind links them so you can see the relationship between writer/reader nodes and the cluster-level endpoints. + +### [`kms-key`](/sources/aws/Types/kms-key) + +When storage encryption is enabled, an RDS instance uses an AWS KMS key to encrypt its underlying EBS volumes and snapshots. The link shows which key protects the data and who can decrypt it. + +### [`iam-role`](/sources/aws/Types/iam-role) + +Features such as S3 import/export, AWS Lambda integration, and CloudWatch Logs require the database service to assume an IAM service role. Overmind lists these roles so you can review permissions the database can exercise in your account. + +### [`iam-instance-profile`](/sources/aws/Types/iam-instance-profile) + +RDS Custom instances (and certain on-host integrations) run on dedicated EC2 instances within your account and therefore use an IAM instance profile. If present, Overmind links the profile to reveal any additional permissions granted to the underlying host. diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-parameter-group.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-parameter-group.md new file mode 100644 index 00000000..ad921286 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-parameter-group.md @@ -0,0 +1,17 @@ +--- +title: RDS Parameter Group +sidebar_label: rds-db-parameter-group +--- + +An Amazon RDS DB parameter group is a container for engine configuration values that determine how a database instance or cluster behaves. By attaching a parameter group to one or more RDS resources you override the engine’s built-in defaults with your own settings, allowing you to tune performance, security and operational behaviour. Changes made to the group are propagated to every associated instance; static parameters take effect after the next reboot, while dynamic parameters may apply immediately. +For a full explanation see the official AWS documentation: [Working with DB parameter groups](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithParamGroups.html). + +**Terrafrom Mappings:** + +- `aws_db_parameter_group.arn` + +## Supported Methods + +- `GET`: Get a parameter group by name +- `LIST`: List all parameter groups +- `SEARCH`: Search for a parameter group by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-subnet-group.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-subnet-group.md new file mode 100644 index 00000000..011d6ec6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-subnet-group.md @@ -0,0 +1,27 @@ +--- +title: RDS Subnet Group +sidebar_label: rds-db-subnet-group +--- + +An RDS DB subnet group is a named collection of one or more subnets that belong to a single Amazon VPC. When you create an Amazon RDS DB instance in a VPC, the subnet group tells RDS which subnets, and therefore which Availability Zones, it may use to provision and maintain the instance. Subnet groups are essential for ensuring high availability and proper network isolation of database workloads. +For full details, see the AWS documentation: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.Subnets.html + +**Terrafrom Mappings:** + +- `aws_db_subnet_group.arn` + +## Supported Methods + +- `GET`: Get a subnet group by name +- `LIST`: List all subnet groups +- `SEARCH`: Search for subnet groups by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The DB subnet group is created within exactly one VPC; its subnets must all belong to this VPC, so the group inherits the VPC’s routing and network-security boundaries. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A DB subnet group is a container for multiple EC2 subnets, typically spanning at least two Availability Zones. Each listed subnet in the group contributes one possible placement zone for RDS DB instances. diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-option-group.md b/docs.overmind.tech/docs/sources/aws/Types/rds-option-group.md new file mode 100644 index 00000000..57217f31 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-option-group.md @@ -0,0 +1,17 @@ +--- +title: RDS Option Group +sidebar_label: rds-option-group +--- + +An Amazon Relational Database Service (RDS) Option Group is a logical container that lets you enable and configure additional features—known as “options”—for an RDS DB instance or cluster. Typical options include Oracle Transparent Data Encryption, SQL Server Audit, MariaDB Audit Plugin and many others that are not activated by default with the engine. By assigning an option group to one or more databases you ensure that each instance inherits the same, centrally-managed configuration, simplifying governance and compliance. +For complete details see the official AWS documentation: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithOptionGroups.html + +**Terrafrom Mappings:** + +- `aws_db_option_group.arn` + +## Supported Methods + +- `GET`: Get an option group by name +- `LIST`: List all RDS option groups +- `SEARCH`: Search for an option group by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/route53-health-check.md b/docs.overmind.tech/docs/sources/aws/Types/route53-health-check.md new file mode 100644 index 00000000..a73f438b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/route53-health-check.md @@ -0,0 +1,23 @@ +--- +title: Route53 Health Check +sidebar_label: route53-health-check +--- + +Amazon Route 53 health checks continuously monitor the availability and latency of your application endpoints (such as web servers, API gateways or other resources) and can automatically trigger DNS fail-over when an endpoint becomes unhealthy. Each health check can also be configured to integrate with Amazon CloudWatch, enabling alerting and automation based on the current health state. +For full details, refer to the official AWS documentation: [Amazon Route 53 Health Checks](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover.html). + +**Terrafrom Mappings:** + +- `aws_route53_health_check.id` + +## Supported Methods + +- `GET`: Get health check by ID +- `LIST`: List all health checks +- `SEARCH`: Search for health checks by ARN + +## Possible Links + +### [`cloudwatch-alarm`](/sources/aws/Types/cloudwatch-alarm) + +A CloudWatch alarm can be created that uses the `HealthCheckStatus` metric emitted for a specific Route 53 health check. This allows the alarm to publish notifications or trigger automated responses whenever the health check reports an unhealthy or healthy state. Overmind therefore records a link from a Route 53 health check to any CloudWatch alarms that reference its ID so you can immediately see which alarms will fire if the check changes status. diff --git a/docs.overmind.tech/docs/sources/aws/Types/route53-hosted-zone.md b/docs.overmind.tech/docs/sources/aws/Types/route53-hosted-zone.md new file mode 100644 index 00000000..b8afe159 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/route53-hosted-zone.md @@ -0,0 +1,25 @@ +--- +title: Hosted Zone +sidebar_label: route53-hosted-zone +--- + +An Amazon Route 53 hosted zone is a container for all of the DNS records that belong to a single domain (for example `example.com`) or a sub-domain. It represents a DNS namespace within Route 53 and is the primary object you create when you want AWS to answer queries for your domain. Hosted zones can be public (resolving queries on the public Internet) or private (resolving only within one or more associated VPCs), and support advanced features such as DNSSEC signing and alias records to AWS resources. +For full details see the AWS documentation: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html + +**Terrafrom Mappings:** + +- `aws_route53_hosted_zone_dnssec.id` +- `aws_route53_zone.zone_id` +- `aws_route53_zone_association.zone_id` + +## Supported Methods + +- `GET`: Get a hosted zone by ID +- `LIST`: List all hosted zones +- `SEARCH`: Search for a hosted zone by ARN + +## Possible Links + +### [`route53-resource-record-set`](/sources/aws/Types/route53-resource-record-set) + +Each hosted zone contains one or more resource record sets. Overmind establishes a link from a Hosted Zone item to the `route53-resource-record-set` items that reside within it, allowing you to explore every DNS record that will be created, modified or deleted as part of a deployment. diff --git a/docs.overmind.tech/docs/sources/aws/Types/route53-resource-record-set.md b/docs.overmind.tech/docs/sources/aws/Types/route53-resource-record-set.md new file mode 100644 index 00000000..89aca6f3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/route53-resource-record-set.md @@ -0,0 +1,27 @@ +--- +title: Route53 Record Set +sidebar_label: route53-resource-record-set +--- + +A Route 53 Resource Record Set represents a single DNS record (or a group of records with the same name and type) that lives inside a specific hosted zone. It defines how Amazon Route 53 answers DNS queries for the associated domain name, including the record type (A, AAAA, CNAME, MX, TXT, SRV, etc.), routing policy (simple, weighted, latency, geolocation, fail-over, multi-value, or alias), time-to-live (TTL) and, optionally, a linked health check. +For full details see the AWS documentation: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RRSet.html + +**Terrafrom Mappings:** + +- `aws_route53_record.arn` +- `aws_route53_record.id` + +## Supported Methods + +- `GET`: Get a resource record set. The ID is the concatenation of the hosted zone, name, and record type (`{hostedZone}.{name}.{type}`) +- `LIST`: List all resource record sets + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +Because a Route 53 record set ultimately becomes a DNS record that can be queried on the public or private internet, each record set naturally maps to an Overmind `dns` item. Following this link lets you see the vendor-agnostic representation of the record (name, type, TTL and value) and how it is consumed by other infrastructure components. + +### [`route53-health-check`](/sources/aws/Types/route53-health-check) + +If the record set is configured with a fail-over, latency, or weighted routing policy that refers to a Route 53 health check, Overmind links the record set to that `route53-health-check` item. This shows the dependency between DNS resolution and the health status of the monitored endpoint, helping you understand how an unhealthy resource could affect name resolution. diff --git a/docs.overmind.tech/docs/sources/aws/Types/s3-bucket.md b/docs.overmind.tech/docs/sources/aws/Types/s3-bucket.md new file mode 100644 index 00000000..afcfb3f3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/s3-bucket.md @@ -0,0 +1,55 @@ +--- +title: S3 Bucket +sidebar_label: s3-bucket +--- + +Amazon S3 (Simple Storage Service) buckets are globally-unique containers used to store and organise objects such as files, logs and backups. Each bucket is created within a specific AWS Region, can be configured with fine-grained access controls, lifecycle rules, encryption, versioning and event notifications, and can serve as the origin for many other AWS services. Full service documentation is available in the AWS User Guide: https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html + +**Terrafrom Mappings:** + +- `aws_s3_bucket_acl.bucket` +- `aws_s3_bucket_analytics_configuration.bucket` +- `aws_s3_bucket_cors_configuration.bucket` +- `aws_s3_bucket_intelligent_tiering_configuration.bucket` +- `aws_s3_bucket_inventory.bucket` +- `aws_s3_bucket_lifecycle_configuration.bucket` +- `aws_s3_bucket_logging.bucket` +- `aws_s3_bucket_metric.bucket` +- `aws_s3_bucket_notification.bucket` +- `aws_s3_bucket_object_lock_configuration.bucket` +- `aws_s3_bucket_object.bucket` +- `aws_s3_bucket_ownership_controls.bucket` +- `aws_s3_bucket_policy.bucket` +- `aws_s3_bucket_public_access_block.bucket` +- `aws_s3_bucket_replication_configuration.bucket` +- `aws_s3_bucket_request_payment_configuration.bucket` +- `aws_s3_bucket_server_side_encryption_configuration.bucket` +- `aws_s3_bucket_versioning.bucket` +- `aws_s3_bucket_website_configuration.bucket` +- `aws_s3_bucket.id` +- `aws_s3_object_copy.bucket` +- `aws_s3_object.bucket` + +## Supported Methods + +- `GET`: Get an S3 bucket by name +- `LIST`: List all S3 buckets +- `SEARCH`: Search for S3 buckets by ARN + +## Possible Links + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +An S3 bucket can invoke Lambda functions through S3 event notifications (e.g. when an object is created, deleted or restored). Overmind surfaces this relationship so that you can identify deployment risks such as circular triggers or permissions gaps between the bucket and the associated Lambda execution role. + +### [`sqs-queue`](/sources/aws/Types/sqs-queue) + +Buckets may be configured to send event notifications to SQS queues. Overmind links the bucket to any target queue, allowing you to assess the impact of queue deletion, encryption settings or IAM policies on the integrity of the event pipeline. + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +Similar to SQS, S3 buckets can publish object-level events to SNS topics. Overmind records this connection so you can verify that topic policies permit delivery and that message fan-out will still function after your planned changes. + +### [`s3-bucket`](/sources/aws/Types/s3-bucket) + +Buckets are often paired through cross-Region replication or configured as website redirects to one another. Overmind creates links between the source and destination buckets to highlight dependencies such as replication roles, encryption configuration compatibility and versioning status. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-data-protection-policy.md b/docs.overmind.tech/docs/sources/aws/Types/sns-data-protection-policy.md new file mode 100644 index 00000000..93e4c5db --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-data-protection-policy.md @@ -0,0 +1,22 @@ +--- +title: SNS Data Protection Policy +sidebar_label: sns-data-protection-policy +--- + +Amazon Simple Notification Service (SNS) is a fully managed messaging service for both application-to-application (A2A) and application-to-person (A2P) communication. SNS topics allow you to fan out messages to a large number of subscribers, including distributed systems, and serverless applications. The SNS Data Protection Policy provides a mechanism to ensure that the data transmitted through SNS is compliant with your organisational and regulatory requirements. This policy is used to define and enforce encryption, data retention, and access control practices on SNS topics. For more details, you can refer to the [official AWS SNS Data Protection documentation](https://docs.aws.amazon.com/sns/latest/dg/sns-data-encryption.html). + +**Terraform Mappings:** + +- `aws_sns_topic_data_protection_policy.arn` + +## Supported Methods + +- `GET`: Get an SNS data protection policy by associated topic ARN +- ~~`LIST`~~ +- `SEARCH`: Search SNS data protection policies by its ARN + +## Possible Links + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +The SNS Data Protection Policy is directly related to SNS topics as it outlines the security measures and data management practices that are applied to messages sent through these topics. By associating a data protection policy with an SNS topic, users can ensure that their SNS workflows adhere to the necessary data protection and compliance standards. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-endpoint.md b/docs.overmind.tech/docs/sources/aws/Types/sns-endpoint.md new file mode 100644 index 00000000..308162a5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-endpoint.md @@ -0,0 +1,12 @@ +--- +title: SNS Endpoint +sidebar_label: sns-endpoint +--- + +The SNS Endpoint resource represents a single destination—typically a mobile device, browser, or desktop application instance—that can receive push notifications through Amazon Simple Notification Service (SNS). Each endpoint is created under a specific Platform Application and is identified by a unique Amazon Resource Name (ARN). Managing endpoints correctly is crucial, as inactive or mis-configured endpoints can lead to failed deliveries, increased costs, or even unwanted data exposure. For full details see the official AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/mobile-push-send-devicetoken.html + +## Supported Methods + +- `GET`: Get an SNS endpoint by its ARN +- ~~`LIST`~~ +- `SEARCH`: Search SNS endpoints by associated Platform Application ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-platform-application.md b/docs.overmind.tech/docs/sources/aws/Types/sns-platform-application.md new file mode 100644 index 00000000..f04a1f58 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-platform-application.md @@ -0,0 +1,23 @@ +--- +title: SNS Platform Application +sidebar_label: sns-platform-application +--- + +An Amazon Simple Notification Service (SNS) **platform application** represents a collection of credentials that allow SNS to send push notifications through a specific mobile push service, such as Apple APNS, Google FCM or Amazon ADM. Once you create a platform application, you can register individual mobile devices (platform endpoints) under it and publish messages that will be delivered to those devices by the relevant push provider. +For a full description see the AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/mobile-push-send.html#mobile-push-sns-platform. + +**Terrafrom Mappings:** + +- `aws_sns_platform_application.id` + +## Supported Methods + +- `GET`: Get an SNS platform application by its ARN +- `LIST`: List all SNS platform applications +- `SEARCH`: Search SNS platform applications by ARN + +## Possible Links + +### [`sns-endpoint`](/sources/aws/Types/sns-endpoint) + +Each platform application can have many child **SNS platform endpoints**—one per registered device. Linking the application to its endpoints lets Overmind surface which devices are affected by configuration changes or credential mis-configurations in the parent application. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-subscription.md b/docs.overmind.tech/docs/sources/aws/Types/sns-subscription.md new file mode 100644 index 00000000..82565304 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-subscription.md @@ -0,0 +1,26 @@ +--- +title: SNS Subscription +sidebar_label: sns-subscription +--- + +An Amazon Simple Notification Service (SNS) subscription represents the association between an SNS topic and the endpoint that receives the messages published to that topic. Each subscription specifies the delivery protocol (e-mail, SMS, HTTP/S, Lambda, SQS, Firehose, etc.), the endpoint address, and optional delivery policies or filter policies that control how and when messages are delivered. For full details see the official AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/sns-subscription.html + +**Terrafrom Mappings:** + +- `aws_sns_topic_subscription.id` + +## Supported Methods + +- `GET`: Get an SNS subscription by its ARN +- `LIST`: List all SNS subscriptions +- `SEARCH`: Search SNS subscription by ARN + +## Possible Links + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +Every subscription belongs to exactly one SNS topic. The subscription’s ARN embeds the topic ARN, and deleting the topic automatically removes the subscription. Overmind links the subscription to its parent `sns-topic` so you can trace message flow from publisher (topic) to consumer (subscription endpoint). + +### [`iam-role`](/sources/aws/Types/iam-role) + +If the subscription delivers to an AWS resource in another account (e.g., cross-account SQS queue, Lambda function, or Kinesis Data Firehose), SNS must assume an IAM role that grants it permission to publish to that resource. Overmind links the subscription to any `iam-role` referenced in its delivery policy to help you verify that the correct cross-account permissions are in place. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-topic.md b/docs.overmind.tech/docs/sources/aws/Types/sns-topic.md new file mode 100644 index 00000000..317b0b13 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-topic.md @@ -0,0 +1,22 @@ +--- +title: SNS Topic +sidebar_label: sns-topic +--- + +An Amazon Simple Notification Service (SNS) topic is a logical access point through which publishers send messages that are then fanned-out to subscribed endpoints such as email addresses, HTTP/S webhooks, Lambda functions or SQS queues. Topics can be configured with attributes such as delivery policies, access control policies and optional server-side encryption using AWS Key Management Service (KMS). For further details refer to the official AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/sns-create-topic.html + +**Terrafrom Mappings:** + +- `aws_sns_topic.id` + +## Supported Methods + +- `GET`: Get an SNS topic by its ARN +- `LIST`: List all SNS topics +- `SEARCH`: Search SNS topic by ARN + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +If server-side encryption is enabled for the SNS topic, it references a KMS customer master key (CMK). This link allows Overmind to surface the relationship between the topic and the key that protects its message payloads in transit and at rest. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sqs-queue.md b/docs.overmind.tech/docs/sources/aws/Types/sqs-queue.md new file mode 100644 index 00000000..14493b73 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sqs-queue.md @@ -0,0 +1,27 @@ +--- +title: SQS Queue +sidebar_label: sqs-queue +--- + +Amazon Simple Queue Service (SQS) provides fully-managed message queues that decouple and scale micro-services, distributed systems and serverless applications. A queue acts as a buffer, reliably storing any amount of messages until they are processed and deleted by consumers. Two delivery modes are available – standard (at-least-once, best-effort ordering) and FIFO (exactly-once, ordered). Queues can be encrypted, configured with dead-letter queues, and integrated with other AWS services such as Lambda or SNS. +For a comprehensive description see the official AWS documentation: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html + +**Terrafrom Mappings:** + +- `aws_sqs_queue.id` + +## Supported Methods + +- `GET`: Get an SQS queue attributes by its URL +- `LIST`: List all SQS queue URLs +- `SEARCH`: Search SQS queue by ARN + +## Possible Links + +### [`http`](/sources/stdlib/Types/http) + +Each SQS queue is identified by an HTTPS URL of the form `https://sqs..amazonaws.com//`. Overmind represents this URL as an `http` item, so the queue is linked to the corresponding `http` item that models the endpoint used by the AWS API. + +### [`lambda-event-source-mapping`](/sources/aws/Types/lambda-event-source-mapping) + +When a Lambda function is configured with an event-source mapping that pulls messages from an SQS queue, Overmind creates a `lambda-event-source-mapping` item. The mapping item is linked to the SQS queue it reads from, allowing impact analysis when either the queue or the Lambda configuration changes. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ssm-parameter.md b/docs.overmind.tech/docs/sources/aws/Types/ssm-parameter.md new file mode 100644 index 00000000..7f776d0f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ssm-parameter.md @@ -0,0 +1,31 @@ +--- +title: SSM Parameter +sidebar_label: ssm-parameter +--- + +AWS Systems Manager (SSM) Parameters, stored in the Systems Manager Parameter Store, provide a centralised, version-controlled repository for configuration data such as plain strings, SecureStrings (encrypted secrets), and hierarchical documents. They allow you to decouple configuration and secrets from code, share settings across services and accounts, and take advantage of fine-grained IAM access controls. See the official AWS documentation for full details: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html + +**Terrafrom Mappings:** + +- `aws_ssm_parameter.name` +- `aws_ssm_parameter.arn` + +## Supported Methods + +- `GET`: Get an SSM parameter by name +- `LIST`: List all SSM parameters +- `SEARCH`: Search for SSM parameters by ARN. This supports ARNs from IAM policies that contain wildcards + +## Possible Links + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +If a parameter’s value represents an IP address or a list of addresses (for example, a whitelist used by a Lambda function or security group rule generator), Overmind will surface a link to the corresponding `ip` entity so that you can trace where the address originates and what else depends on it. + +### [`http`](/sources/stdlib/Types/http) + +Parameters often store URLs for upstream APIs, S3 buckets, or internal services. When the value of a parameter matches an HTTP or HTTPS URL, Overmind creates an `http` link, enabling you to follow the dependency chain from the configuration to the external or internal endpoint. + +### [`dns`](/sources/stdlib/Types/dns) + +Likewise, when a parameter’s value contains a hostname or FQDN, Overmind links it to the relevant `dns` record. This makes it easy to assess the impact of DNS changes on applications that retrieve their endpoint addresses from Parameter Store. diff --git a/docs.overmind.tech/docs/sources/aws/_category_.json b/docs.overmind.tech/docs/sources/aws/_category_.json new file mode 100644 index 00000000..3e7fb1b9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "AWS", + "position": 2, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to integrate your AWS" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/account_settings.png b/docs.overmind.tech/docs/sources/aws/account_settings.png new file mode 100644 index 00000000..a244a522 Binary files /dev/null and b/docs.overmind.tech/docs/sources/aws/account_settings.png differ diff --git a/docs.overmind.tech/docs/sources/aws/aws_manual.png b/docs.overmind.tech/docs/sources/aws/aws_manual.png new file mode 100644 index 00000000..9a01d077 Binary files /dev/null and b/docs.overmind.tech/docs/sources/aws/aws_manual.png differ diff --git a/docs.overmind.tech/docs/sources/aws/aws_source_settings.png b/docs.overmind.tech/docs/sources/aws/aws_source_settings.png new file mode 100644 index 00000000..f8f38128 Binary files /dev/null and b/docs.overmind.tech/docs/sources/aws/aws_source_settings.png differ diff --git a/docs.overmind.tech/docs/sources/aws/cloudformation-update-stack.png b/docs.overmind.tech/docs/sources/aws/cloudformation-update-stack.png new file mode 100644 index 00000000..f5ee8568 Binary files /dev/null and b/docs.overmind.tech/docs/sources/aws/cloudformation-update-stack.png differ diff --git a/docs.overmind.tech/docs/sources/aws/configuration.md b/docs.overmind.tech/docs/sources/aws/configuration.md new file mode 100644 index 00000000..9d2c115a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/configuration.md @@ -0,0 +1,118 @@ +--- +title: AWS Configuration +sidebar_position: 1 +--- + +To be able to analyse and discover your infrastructure, Overmind requires read-only access to your AWS account. There are two ways to configure this: + +- **Temporarily:** When you run the `overmind terraform` commands locally, the CLI uses the same AWS access that Terraform does to create a temporary local source. This gives Overmind access to AWS while the command is running, but not afterwards. +- **Permanently** (Recommended): This is known as a "Managed Source". Managed sources are always running and assume an IAM role that you create in your AWS account that gives them read-only AWS access. + +## Configure a Managed Source + +To create an AWS source, open [settings](https://app.overmind.tech/settings) by clicking your profile picture in the top right of the screen, then clicking Account Settings, then [Sources](https://app.overmind.tech/settings/sources) + +![Screenshot of the "User settings" menu, showing the first steps to take: Click "Account Settings"](./account_settings.png) + +Then click Add Source > AWS. + +![Screenshot of the sources subsection of the Overmind settings with the Add Source > AWS button highlighted](./aws_source_settings.png) + +Then, use "Deploy with AWS CloudFormation" to be taken to the AWS console. You may need to sign in and reload the page. With the results from the CloudFormation deployment, choose a name for your source (e.g. "prod") and fill in "Region" and "AWSTargetRoleARN". + +![Screenshot of the "Add AWS Source" dialogue, showing tabs for automatic and manual setup. The automatic setup pane is selected. There is explanation text and input fields for Source name, Region and AWSTargetRoleARN.](./configure-aws.png) + +Press "Create source" to finish the configuration. + +## Manual Setup + +To allow Overmind to access your infrastructure safely, you need to first configure a role and trust relationship that the Overmind AWS account can assume. + +This role will be protected by an [external ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html#external-id-purpose). + +To create the role, open the AWS console for the account you wish to link to Overmind, then: + +1. Open IAM +1. Click Roles +1. Click "Create role" +1. In "Trusted entity type" select "AWS account" +1. In "An AWS account" select "Another AWS account" and enter `942836531449` +1. (Optional, you can do this later) Tick "Require external ID". **Note:** Each source within Overmind has its own unique external ID. In order to find the external ID for a source go to Settings > Sources > Add Source > AWS > Manual Setup and copy the external ID from Step 3. Do not close this window after you have done this, you'll need it later +1. On the "Add permissions", don't select anything, just click "Next" +1. In "Role name" enter a descriptive name like `overmind-read-only` +1. Click "Create Role" + +The next step is to assign permissions to this role. To do this open your newly created role, then: + +1. Click "Add Permissions" > "Create inline policy" +1. Select JSON +1. Paste the following policy: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "apigateway:Get*", + "autoscaling:Describe*", + "cloudfront:Get*", + "cloudfront:List*", + "cloudwatch:Describe*", + "cloudwatch:GetMetricData", + "cloudwatch:ListTagsForResource", + "directconnect:Describe*", + "dynamodb:Describe*", + "dynamodb:List*", + "ec2:Describe*", + "ecs:Describe*", + "ecs:List*", + "eks:Describe*", + "eks:List*", + "elasticfilesystem:Describe*", + "elasticloadbalancing:Describe*", + "iam:Get*", + "iam:List*", + "kms:Describe*", + "kms:Get*", + "kms:List*", + "lambda:Get*", + "lambda:List*", + "network-firewall:Describe*", + "network-firewall:List*", + "networkmanager:Describe*", + "networkmanager:Get*", + "networkmanager:List*", + "rds:Describe*", + "rds:ListTagsForResource", + "route53:Get*", + "route53:List*", + "s3:GetBucket*", + "s3:ListAllMyBuckets", + "sns:Get*", + "sns:List*", + "sqs:Get*", + "sqs:List*", + "ssm:Describe*", + "ssm:Get*", + "ssm:ListTagsForResource" + ], + "Resource": "*" + } + ] + } + ``` + +1. Name the policy `overmind-read-only` +1. Click "Create policy" + +At this point the permissions are complete, the last step is to copy the ARN of the role from the IAM console, and paste it back into Overmind, and create the source. The source will get a green tick once it's started and connected, which should take less than a minute. + +## Check your sources + +After you have configured a source, it'll show up in the [Source Settings](https://app.overmind.tech/changes?settings=1&activeTab=sources). There you can check that the source is healthy. + +## Explore your new data + +Once your new source is healthy, jump over to the [Explore page](https://app.overmind.tech/explore?type=*&method=LIST&linkDepth=1) to show all your resources. diff --git a/docs.overmind.tech/docs/sources/aws/configure-aws.png b/docs.overmind.tech/docs/sources/aws/configure-aws.png new file mode 100644 index 00000000..e2548560 Binary files /dev/null and b/docs.overmind.tech/docs/sources/aws/configure-aws.png differ diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-api-key.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-api-key.json new file mode 100644 index 00000000..6d9ab96c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-api-key.json @@ -0,0 +1,18 @@ +{ + "type": "apigateway-api-key", + "category": 4, + "descriptiveName": "API Key", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Key by ID", + "list": true, + "listDescription": "List all API Keys", + "search": true, + "searchDescription": "Search for API Keys by their name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_api_key.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-authorizer.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-authorizer.json new file mode 100644 index 00000000..d83a67fe --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-authorizer.json @@ -0,0 +1,16 @@ +{ + "type": "apigateway-authorizer", + "category": 4, + "descriptiveName": "API Gateway Authorizer", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Gateway Authorizer by its rest API ID and ID: rest-api-id/authorizer-id", + "search": true, + "searchDescription": "Search for API Gateway Authorizers by their rest API ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_authorizer.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-deployment.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-deployment.json new file mode 100644 index 00000000..cac6b30f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-deployment.json @@ -0,0 +1,16 @@ +{ + "type": "apigateway-deployment", + "category": 7, + "descriptiveName": "API Gateway Deployment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Gateway Deployment by its rest API ID and ID: rest-api-id/deployment-id", + "search": true, + "searchDescription": "Search for API Gateway Deployments by their rest API ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_deployment.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-domain-name.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-domain-name.json new file mode 100644 index 00000000..412b9a24 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-domain-name.json @@ -0,0 +1,19 @@ +{ + "type": "apigateway-domain-name", + "category": 1, + "potentialLinks": ["acm-certificate"], + "descriptiveName": "API Gateway Domain Name", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Domain Name by domain-name", + "list": true, + "listDescription": "List Domain Names", + "search": true, + "searchDescription": "Search Domain Names by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_domain_name.domain_name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-integration.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-integration.json new file mode 100644 index 00000000..9094090d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-integration.json @@ -0,0 +1,11 @@ +{ + "type": "apigateway-integration", + "category": 3, + "descriptiveName": "API Gateway Integration", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an Integration by rest-api id, resource id, and http-method", + "search": true, + "searchDescription": "Search Integrations by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-method-response.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-method-response.json new file mode 100644 index 00000000..c1fd53f7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-method-response.json @@ -0,0 +1,11 @@ +{ + "type": "apigateway-method-response", + "category": 3, + "descriptiveName": "API Gateway Method Response", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Method Response by it's ID: {rest-api-id}/{resource-id}/{http-method}/{status-code}", + "search": true, + "searchDescription": "Search Method Responses by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-method.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-method.json new file mode 100644 index 00000000..e38a3a81 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-method.json @@ -0,0 +1,17 @@ +{ + "type": "apigateway-method", + "category": 3, + "potentialLinks": [ + "apigateway-integration", + "apigateway-authorizer", + "apigateway-request-validator", + "apigateway-method-response" + ], + "descriptiveName": "API Gateway Method", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Method by it's ID: {rest-api-id}/{resource-id}/{http-method}", + "search": true, + "searchDescription": "Search Methods by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-model.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-model.json new file mode 100644 index 00000000..8b42b277 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-model.json @@ -0,0 +1,16 @@ +{ + "type": "apigateway-model", + "category": 7, + "descriptiveName": "API Gateway Model", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Gateway Model by its rest API ID and model name: rest-api-id/model-name", + "search": true, + "searchDescription": "Search for API Gateway Models by their rest API ID: rest-api-id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_model.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-resource.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-resource.json new file mode 100644 index 00000000..aa66b3d3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-resource.json @@ -0,0 +1,17 @@ +{ + "type": "apigateway-resource", + "category": 1, + "potentialLinks": ["apigateway-method"], + "descriptiveName": "API Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Resource by rest-api-id/resource-id", + "search": true, + "searchDescription": "Search Resources by REST API ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_resource.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-rest-api.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-rest-api.json new file mode 100644 index 00000000..6aff4a93 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-rest-api.json @@ -0,0 +1,19 @@ +{ + "type": "apigateway-rest-api", + "category": 1, + "potentialLinks": ["ec2-vpc-endpoint", "apigateway-resource"], + "descriptiveName": "REST API", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a REST API by ID", + "list": true, + "listDescription": "List all REST APIs", + "search": true, + "searchDescription": "Search for REST APIs by their name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_rest_api.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-stage.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-stage.json new file mode 100644 index 00000000..b165df22 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-stage.json @@ -0,0 +1,17 @@ +{ + "type": "apigateway-stage", + "category": 7, + "potentialLinks": ["wafv2-web-acl"], + "descriptiveName": "API Gateway Stage", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Gateway Stage by its rest API ID and stage name: rest-api-id/stage-name", + "search": true, + "searchDescription": "Search for API Gateway Stages by their rest API ID or with rest API ID and deployment-id: rest-api-id/deployment-id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_stage.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/autoscaling-auto-scaling-group.json b/docs.overmind.tech/docs/sources/aws/data/autoscaling-auto-scaling-group.json new file mode 100644 index 00000000..1404fe1a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/autoscaling-auto-scaling-group.json @@ -0,0 +1,27 @@ +{ + "type": "autoscaling-auto-scaling-group", + "category": 7, + "potentialLinks": [ + "ec2-launch-template", + "elbv2-target-group", + "ec2-instance", + "iam-role", + "autoscaling-launch-configuration", + "ec2-placement-group" + ], + "descriptiveName": "Autoscaling Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an Autoscaling Group by name", + "list": true, + "listDescription": "List Autoscaling Groups", + "search": true, + "searchDescription": "Search for Autoscaling Groups by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_autoscaling_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-cache-policy.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-cache-policy.json new file mode 100644 index 00000000..129c0164 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-cache-policy.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-cache-policy", + "category": 7, + "descriptiveName": "CloudFront Cache Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a CloudFront Cache Policy", + "list": true, + "listDescription": "List CloudFront Cache Policies", + "search": true, + "searchDescription": "Search CloudFront Cache Policies by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_cache_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-continuous-deployment-policy.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-continuous-deployment-policy.json new file mode 100644 index 00000000..7c0164eb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-continuous-deployment-policy.json @@ -0,0 +1,14 @@ +{ + "type": "cloudfront-continuous-deployment-policy", + "category": 7, + "potentialLinks": ["dns"], + "descriptiveName": "CloudFront Continuous Deployment Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a CloudFront Continuous Deployment Policy by ID", + "list": true, + "listDescription": "List CloudFront Continuous Deployment Policies", + "search": true, + "searchDescription": "Search CloudFront Continuous Deployment Policies by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-distribution.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-distribution.json new file mode 100644 index 00000000..ec2973c9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-distribution.json @@ -0,0 +1,33 @@ +{ + "type": "cloudfront-distribution", + "category": 3, + "potentialLinks": [ + "cloudfront-key-group", + "cloudfront-cloud-front-origin-access-identity", + "cloudfront-continuous-deployment-policy", + "cloudfront-cache-policy", + "cloudfront-field-level-encryption", + "cloudfront-function", + "cloudfront-origin-request-policy", + "cloudfront-realtime-log-config", + "cloudfront-response-headers-policy", + "dns", + "lambda-function", + "s3-bucket" + ], + "descriptiveName": "CloudFront Distribution", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a distribution by ID", + "list": true, + "listDescription": "List all distributions", + "search": true, + "searchDescription": "Search distributions by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_cloudfront_distribution.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-function.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-function.json new file mode 100644 index 00000000..d2583d2f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-function.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-function", + "category": 1, + "descriptiveName": "CloudFront Function", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a CloudFront Function by name", + "list": true, + "listDescription": "List CloudFront Functions", + "search": true, + "searchDescription": "Search CloudFront Functions by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_function.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-key-group.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-key-group.json new file mode 100644 index 00000000..2172ce2c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-key-group.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-key-group", + "category": 7, + "descriptiveName": "CloudFront Key Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a CloudFront Key Group by ID", + "list": true, + "listDescription": "List CloudFront Key Groups", + "search": true, + "searchDescription": "Search CloudFront Key Groups by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_key_group.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-access-control.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-access-control.json new file mode 100644 index 00000000..50e96263 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-access-control.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-origin-access-control", + "category": 4, + "descriptiveName": "Cloudfront Origin Access Control", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get Origin Access Control by ID", + "list": true, + "listDescription": "List Origin Access Controls", + "search": true, + "searchDescription": "Origin Access Control by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_origin_access_control.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-request-policy.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-request-policy.json new file mode 100644 index 00000000..76ec63c2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-request-policy.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-origin-request-policy", + "category": 3, + "descriptiveName": "CloudFront Origin Request Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get Origin Request Policy by ID", + "list": true, + "listDescription": "List Origin Request Policies", + "search": true, + "searchDescription": "Origin Request Policy by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_origin_request_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-realtime-log-config.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-realtime-log-config.json new file mode 100644 index 00000000..5b26ad55 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-realtime-log-config.json @@ -0,0 +1,19 @@ +{ + "type": "cloudfront-realtime-log-config", + "category": 7, + "descriptiveName": "CloudFront Realtime Log Config", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get Realtime Log Config by Name", + "list": true, + "listDescription": "List Realtime Log Configs", + "search": true, + "searchDescription": "Search Realtime Log Configs by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_cloudfront_realtime_log_config.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-response-headers-policy.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-response-headers-policy.json new file mode 100644 index 00000000..5eb9dd3e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-response-headers-policy.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-response-headers-policy", + "category": 3, + "descriptiveName": "CloudFront Response Headers Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get Response Headers Policy by ID", + "list": true, + "listDescription": "List Response Headers Policies", + "search": true, + "searchDescription": "Search Response Headers Policy by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_response_headers_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-streaming-distribution.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-streaming-distribution.json new file mode 100644 index 00000000..5443b821 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-streaming-distribution.json @@ -0,0 +1,23 @@ +{ + "type": "cloudfront-streaming-distribution", + "category": 3, + "potentialLinks": ["dns"], + "descriptiveName": "CloudFront Streaming Distribution", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Streaming Distribution by ID", + "list": true, + "listDescription": "List Streaming Distributions", + "search": true, + "searchDescription": "Search Streaming Distributions by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_cloudfront_distribution.arn" + }, + { + "terraformQueryMap": "aws_cloudfront_distribution.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudwatch-alarm.json b/docs.overmind.tech/docs/sources/aws/data/cloudwatch-alarm.json new file mode 100644 index 00000000..1dd40323 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudwatch-alarm.json @@ -0,0 +1,19 @@ +{ + "type": "cloudwatch-alarm", + "category": 5, + "potentialLinks": ["cloudwatch-metric"], + "descriptiveName": "CloudWatch Alarm", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an alarm by name", + "list": true, + "listDescription": "List all alarms", + "search": true, + "searchDescription": "Search for alarms. This accepts JSON in the format of `cloudwatch.DescribeAlarmsForMetricInput`" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudwatch_metric_alarm.alarm_name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-connection.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-connection.json new file mode 100644 index 00000000..036a6813 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-connection.json @@ -0,0 +1,24 @@ +{ + "type": "directconnect-connection", + "category": 3, + "potentialLinks": [ + "directconnect-lag", + "directconnect-location", + "directconnect-loa", + "directconnect-virtual-interface" + ], + "descriptiveName": "Connection", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a connection by ID", + "list": true, + "listDescription": "List all connections", + "search": true, + "searchDescription": "Search connection by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_connection.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-customer-metadata.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-customer-metadata.json new file mode 100644 index 00000000..6a818208 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-customer-metadata.json @@ -0,0 +1,13 @@ +{ + "type": "directconnect-customer-metadata", + "category": 7, + "descriptiveName": "Customer Metadata", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a customer agreement by name", + "list": true, + "listDescription": "List all customer agreements", + "search": true, + "searchDescription": "Search customer agreements by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association-proposal.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association-proposal.json new file mode 100644 index 00000000..57ce90e8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association-proposal.json @@ -0,0 +1,19 @@ +{ + "type": "directconnect-direct-connect-gateway-association-proposal", + "category": 7, + "potentialLinks": ["directconnect-direct-connect-gateway-association"], + "descriptiveName": "Direct Connect Gateway Association Proposal", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Direct Connect Gateway Association Proposal by ID", + "list": true, + "listDescription": "List all Direct Connect Gateway Association Proposals", + "search": true, + "searchDescription": "Search Direct Connect Gateway Association Proposals by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_gateway_association_proposal.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association.json new file mode 100644 index 00000000..62c19590 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association.json @@ -0,0 +1,17 @@ +{ + "type": "directconnect-direct-connect-gateway-association", + "category": 3, + "potentialLinks": ["directconnect-direct-connect-gateway"], + "descriptiveName": "Direct Connect Gateway Association", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a direct connect gateway association by direct connect gateway ID and virtual gateway ID", + "search": true, + "searchDescription": "Search direct connect gateway associations by direct connect gateway ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_gateway_association.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-attachment.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-attachment.json new file mode 100644 index 00000000..d4d7a711 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-attachment.json @@ -0,0 +1,15 @@ +{ + "type": "directconnect-direct-connect-gateway-attachment", + "category": 3, + "potentialLinks": [ + "directconnect-direct-connect-gateway", + "directconnect-virtual-interface" + ], + "descriptiveName": "Direct Connect Gateway Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a direct connect gateway attachment by DirectConnectGatewayId/VirtualInterfaceId", + "search": true, + "searchDescription": "Search direct connect gateway attachments for given VirtualInterfaceId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway.json new file mode 100644 index 00000000..e35ae42a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway.json @@ -0,0 +1,18 @@ +{ + "type": "directconnect-direct-connect-gateway", + "category": 3, + "descriptiveName": "Direct Connect Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a direct connect gateway by ID", + "list": true, + "listDescription": "List all direct connect gateways", + "search": true, + "searchDescription": "Search direct connect gateway by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_gateway.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-hosted-connection.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-hosted-connection.json new file mode 100644 index 00000000..d8502036 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-hosted-connection.json @@ -0,0 +1,22 @@ +{ + "type": "directconnect-hosted-connection", + "category": 3, + "potentialLinks": [ + "directconnect-lag", + "directconnect-location", + "directconnect-loa", + "directconnect-virtual-interface" + ], + "descriptiveName": "Hosted Connection", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Hosted Connection by connection ID", + "search": true, + "searchDescription": "Search Hosted Connections by Interconnect or LAG ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_hosted_connection.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-interconnect.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-interconnect.json new file mode 100644 index 00000000..33c50a03 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-interconnect.json @@ -0,0 +1,19 @@ +{ + "type": "directconnect-interconnect", + "category": 3, + "potentialLinks": [ + "directconnect-hosted-connection", + "directconnect-lag", + "directconnect-loa", + "directconnect-location" + ], + "descriptiveName": "Interconnect", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Interconnect by InterconnectId", + "list": true, + "listDescription": "List all Interconnects", + "search": true, + "searchDescription": "Search Interconnects by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-lag.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-lag.json new file mode 100644 index 00000000..6080c105 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-lag.json @@ -0,0 +1,23 @@ +{ + "type": "directconnect-lag", + "category": 3, + "potentialLinks": [ + "directconnect-connection", + "directconnect-hosted-connection", + "directconnect-location" + ], + "descriptiveName": "Link Aggregation Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Link Aggregation Group by ID", + "list": true, + "listDescription": "List all Link Aggregation Groups", + "search": true, + "searchDescription": "Search Link Aggregation Group by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_lag.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-location.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-location.json new file mode 100644 index 00000000..626e8c19 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-location.json @@ -0,0 +1,18 @@ +{ + "type": "directconnect-location", + "category": 3, + "descriptiveName": "Direct Connect Location", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Location by its code", + "list": true, + "listDescription": "List all Direct Connect Locations", + "search": true, + "searchDescription": "Search Direct Connect Locations by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_location.location_code" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-router-configuration.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-router-configuration.json new file mode 100644 index 00000000..05264ca9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-router-configuration.json @@ -0,0 +1,17 @@ +{ + "type": "directconnect-router-configuration", + "category": 7, + "potentialLinks": ["directconnect-virtual-interface"], + "descriptiveName": "Router Configuration", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Router Configuration by Virtual Interface ID", + "search": true, + "searchDescription": "Search Router Configuration by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_router_configuration.virtual_interface_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-gateway.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-gateway.json new file mode 100644 index 00000000..d3b7caf7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-gateway.json @@ -0,0 +1,13 @@ +{ + "type": "directconnect-virtual-gateway", + "category": 3, + "descriptiveName": "Direct Connect Virtual Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a virtual gateway by ID", + "list": true, + "listDescription": "List all virtual gateways", + "search": true, + "searchDescription": "Search virtual gateways by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-interface.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-interface.json new file mode 100644 index 00000000..30edf074 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-interface.json @@ -0,0 +1,31 @@ +{ + "type": "directconnect-virtual-interface", + "category": 3, + "potentialLinks": [ + "directconnect-connection", + "directconnect-direct-connect-gateway", + "rdap-ip-network", + "directconnect-direct-connect-gateway-attachment", + "directconnect-virtual-interface" + ], + "descriptiveName": "Virtual Interface", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a virtual interface by ID", + "list": true, + "listDescription": "List all virtual interfaces", + "search": true, + "searchDescription": "Search virtual interfaces by connection ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_private_virtual_interface.id" + }, + { + "terraformQueryMap": "aws_dx_public_virtual_interface.id" + }, + { + "terraformQueryMap": "aws_dx_transit_virtual_interface.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/dynamodb-backup.json b/docs.overmind.tech/docs/sources/aws/data/dynamodb-backup.json new file mode 100644 index 00000000..bbef222b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/dynamodb-backup.json @@ -0,0 +1,12 @@ +{ + "type": "dynamodb-backup", + "category": 6, + "potentialLinks": ["dynamodb-table"], + "descriptiveName": "DynamoDB Backup", + "supportedQueryMethods": { + "list": true, + "listDescription": "List all DynamoDB backups", + "search": true, + "searchDescription": "Search for a DynamoDB backup by table name" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/dynamodb-table.json b/docs.overmind.tech/docs/sources/aws/data/dynamodb-table.json new file mode 100644 index 00000000..2f9933e3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/dynamodb-table.json @@ -0,0 +1,25 @@ +{ + "type": "dynamodb-table", + "category": 6, + "potentialLinks": [ + "kinesis-stream", + "backup-recovery-point", + "dynamodb-table", + "kms-key" + ], + "descriptiveName": "DynamoDB Table", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a DynamoDB table by name", + "list": true, + "listDescription": "List all DynamoDB tables", + "search": true, + "searchDescription": "Search for DynamoDB tables by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_dynamodb_table.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-address.json b/docs.overmind.tech/docs/sources/aws/data/ec2-address.json new file mode 100644 index 00000000..9248b320 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-address.json @@ -0,0 +1,22 @@ +{ + "type": "ec2-address", + "category": 3, + "potentialLinks": ["ec2-instance", "ip", "ec2-network-interface"], + "descriptiveName": "EC2 Address", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an EC2 address by Public IP", + "list": true, + "listDescription": "List EC2 addresses", + "search": true, + "searchDescription": "Search for EC2 addresses by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_eip.public_ip" + }, + { + "terraformQueryMap": "aws_eip_association.public_ip" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation-fleet.json b/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation-fleet.json new file mode 100644 index 00000000..e2758bb3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation-fleet.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-capacity-reservation-fleet", + "category": 7, + "potentialLinks": ["ec2-capacity-reservation"], + "descriptiveName": "Capacity Reservation Fleet", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a capacity reservation fleet by ID", + "list": true, + "listDescription": "List capacity reservation fleets", + "search": true, + "searchDescription": "Search capacity reservation fleets by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation.json b/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation.json new file mode 100644 index 00000000..e2a9ec25 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation.json @@ -0,0 +1,23 @@ +{ + "type": "ec2-capacity-reservation", + "category": 7, + "potentialLinks": [ + "outposts-outpost", + "ec2-placement-group", + "ec2-capacity-reservation-fleet" + ], + "descriptiveName": "Capacity Reservation", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a capacity reservation fleet by ID", + "list": true, + "listDescription": "List capacity reservation fleets", + "search": true, + "searchDescription": "Search capacity reservation fleets by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ec2_capacity_reservation_fleet.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-egress-only-internet-gateway.json b/docs.overmind.tech/docs/sources/aws/data/ec2-egress-only-internet-gateway.json new file mode 100644 index 00000000..4a9002d8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-egress-only-internet-gateway.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-egress-only-internet-gateway", + "category": 3, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "Egress Only Internet Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an egress only internet gateway by ID", + "list": true, + "listDescription": "List all egress only internet gateways", + "search": true, + "searchDescription": "Search egress only internet gateways by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "egress_only_internet_gateway.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-iam-instance-profile-association.json b/docs.overmind.tech/docs/sources/aws/data/ec2-iam-instance-profile-association.json new file mode 100644 index 00000000..c76f2643 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-iam-instance-profile-association.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-iam-instance-profile-association", + "category": 4, + "potentialLinks": ["iam-instance-profile", "ec2-instance"], + "descriptiveName": "IAM Instance Profile Association", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM Instance Profile Association by ID", + "list": true, + "listDescription": "List all IAM Instance Profile Associations", + "search": true, + "searchDescription": "Search IAM Instance Profile Associations by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-image.json b/docs.overmind.tech/docs/sources/aws/data/ec2-image.json new file mode 100644 index 00000000..b7bf0b29 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-image.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-image", + "category": 1, + "descriptiveName": "Amazon Machine Image (AMI)", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an AMI by ID", + "list": true, + "listDescription": "List all AMIs", + "search": true, + "searchDescription": "Search AMIs by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ami.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-instance-event-window.json b/docs.overmind.tech/docs/sources/aws/data/ec2-instance-event-window.json new file mode 100644 index 00000000..42d660f7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-instance-event-window.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-instance-event-window", + "category": 7, + "potentialLinks": ["ec2-host", "ec2-instance"], + "descriptiveName": "EC2 Instance Event Window", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an event window by ID", + "list": true, + "listDescription": "List all event windows", + "search": true, + "searchDescription": "Search for event windows by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-instance-status.json b/docs.overmind.tech/docs/sources/aws/data/ec2-instance-status.json new file mode 100644 index 00000000..fb1af5cb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-instance-status.json @@ -0,0 +1,13 @@ +{ + "type": "ec2-instance-status", + "category": 5, + "descriptiveName": "EC2 Instance Status", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an EC2 instance status by Instance ID", + "list": true, + "listDescription": "List all EC2 instance statuses", + "search": true, + "searchDescription": "Search EC2 instance statuses by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-instance.json b/docs.overmind.tech/docs/sources/aws/data/ec2-instance.json new file mode 100644 index 00000000..52182893 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-instance.json @@ -0,0 +1,41 @@ +{ + "type": "ec2-instance", + "category": 1, + "potentialLinks": [ + "ec2-instance-status", + "iam-instance-profile", + "ec2-capacity-reservation", + "ec2-elastic-gpu", + "elastic-inference-accelerator", + "license-manager-license-configuration", + "outposts-outpost", + "ec2-spot-instance-request", + "ec2-image", + "ec2-key-pair", + "ec2-placement-group", + "ip", + "ec2-subnet", + "ec2-vpc", + "dns", + "ec2-security-group", + "ec2-volume" + ], + "descriptiveName": "EC2 Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an EC2 instance by ID", + "list": true, + "listDescription": "List all EC2 instances", + "search": true, + "searchDescription": "Search EC2 instances by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_instance.id" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_instance.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-internet-gateway.json b/docs.overmind.tech/docs/sources/aws/data/ec2-internet-gateway.json new file mode 100644 index 00000000..1df593a0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-internet-gateway.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-internet-gateway", + "category": 3, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "Internet Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an internet gateway by ID", + "list": true, + "listDescription": "List all internet gateways", + "search": true, + "searchDescription": "Search internet gateways by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_internet_gateway.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-key-pair.json b/docs.overmind.tech/docs/sources/aws/data/ec2-key-pair.json new file mode 100644 index 00000000..eb91caf5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-key-pair.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-key-pair", + "category": 4, + "descriptiveName": "Key Pair", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a key pair by name", + "list": true, + "listDescription": "List all key pairs", + "search": true, + "searchDescription": "Search for key pairs by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_key_pair.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template-version.json b/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template-version.json new file mode 100644 index 00000000..1a256942 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template-version.json @@ -0,0 +1,25 @@ +{ + "type": "ec2-launch-template-version", + "category": 1, + "potentialLinks": [ + "ec2-network-interface", + "ec2-subnet", + "ec2-security-group", + "ec2-image", + "ec2-key-pair", + "ec2-snapshot", + "ec2-capacity-reservation", + "ec2-placement-group", + "ec2-host", + "ip" + ], + "descriptiveName": "Launch Template Version", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a launch template version by {templateId}.{version}", + "list": true, + "listDescription": "List all launch template versions", + "search": true, + "searchDescription": "Search launch template versions by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template.json b/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template.json new file mode 100644 index 00000000..52357a89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-launch-template", + "category": 1, + "descriptiveName": "Launch Template", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a launch template by ID", + "list": true, + "listDescription": "List all launch templates", + "search": true, + "searchDescription": "Search for launch templates by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_launch_template.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-nat-gateway.json b/docs.overmind.tech/docs/sources/aws/data/ec2-nat-gateway.json new file mode 100644 index 00000000..2cda0300 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-nat-gateway.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-nat-gateway", + "category": 3, + "potentialLinks": ["ec2-vpc", "ec2-subnet", "ec2-network-interface", "ip"], + "descriptiveName": "NAT Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a NAT Gateway by ID", + "list": true, + "listDescription": "List all NAT gateways", + "search": true, + "searchDescription": "Search for NAT gateways by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_nat_gateway.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-network-acl.json b/docs.overmind.tech/docs/sources/aws/data/ec2-network-acl.json new file mode 100644 index 00000000..3c776a4a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-network-acl.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-network-acl", + "category": 4, + "potentialLinks": ["ec2-subnet", "ec2-vpc"], + "descriptiveName": "Network ACL", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a network ACL", + "list": true, + "listDescription": "List all network ACLs", + "search": true, + "searchDescription": "Search for network ACLs by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_network_acl.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface-permission.json b/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface-permission.json new file mode 100644 index 00000000..e8406928 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface-permission.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-network-interface-permission", + "category": 4, + "potentialLinks": ["ec2-network-interface"], + "descriptiveName": "Network Interface Permission", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a network interface permission by ID", + "list": true, + "listDescription": "List all network interface permissions", + "search": true, + "searchDescription": "Search network interface permissions by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface.json b/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface.json new file mode 100644 index 00000000..46d71875 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface.json @@ -0,0 +1,26 @@ +{ + "type": "ec2-network-interface", + "category": 3, + "potentialLinks": [ + "ec2-instance", + "ec2-security-group", + "ip", + "dns", + "ec2-subnet", + "ec2-vpc" + ], + "descriptiveName": "EC2 Network Interface", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a network interface by ID", + "list": true, + "listDescription": "List all network interfaces", + "search": true, + "searchDescription": "Search network interfaces by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_network_interface.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-placement-group.json b/docs.overmind.tech/docs/sources/aws/data/ec2-placement-group.json new file mode 100644 index 00000000..62c7b33a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-placement-group.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-placement-group", + "category": 1, + "descriptiveName": "Placement Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a placement group by ID", + "list": true, + "listDescription": "List all placement groups", + "search": true, + "searchDescription": "Search for placement groups by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_placement_group.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-reserved-instance.json b/docs.overmind.tech/docs/sources/aws/data/ec2-reserved-instance.json new file mode 100644 index 00000000..539b9d30 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-reserved-instance.json @@ -0,0 +1,13 @@ +{ + "type": "ec2-reserved-instance", + "category": 1, + "descriptiveName": "Reserved EC2 Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a reserved EC2 instance by ID", + "list": true, + "listDescription": "List all reserved EC2 instances", + "search": true, + "searchDescription": "Search reserved EC2 instances by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-route-table.json b/docs.overmind.tech/docs/sources/aws/data/ec2-route-table.json new file mode 100644 index 00000000..83eec9ee --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-route-table.json @@ -0,0 +1,41 @@ +{ + "type": "ec2-route-table", + "category": 3, + "potentialLinks": [ + "ec2-vpc", + "ec2-subnet", + "ec2-internet-gateway", + "ec2-vpc-endpoint", + "ec2-carrier-gateway", + "ec2-egress-only-internet-gateway", + "ec2-instance", + "ec2-local-gateway", + "ec2-nat-gateway", + "ec2-network-interface", + "ec2-transit-gateway", + "ec2-vpc-peering-connection" + ], + "descriptiveName": "Route Table", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a route table by ID", + "list": true, + "listDescription": "List all route tables", + "search": true, + "searchDescription": "Search route tables by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_route_table.id" + }, + { + "terraformQueryMap": "aws_route_table_association.route_table_id" + }, + { + "terraformQueryMap": "aws_default_route_table.default_route_table_id" + }, + { + "terraformQueryMap": "aws_route.route_table_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-security-group-rule.json b/docs.overmind.tech/docs/sources/aws/data/ec2-security-group-rule.json new file mode 100644 index 00000000..d497780b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-security-group-rule.json @@ -0,0 +1,25 @@ +{ + "type": "ec2-security-group-rule", + "category": 4, + "potentialLinks": ["ec2-security-group"], + "descriptiveName": "Security Group Rule", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a security group rule by ID", + "list": true, + "listDescription": "List all security group rules", + "search": true, + "searchDescription": "Search security group rules by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_security_group_rule.security_group_rule_id" + }, + { + "terraformQueryMap": "aws_vpc_security_group_ingress_rule.security_group_rule_id" + }, + { + "terraformQueryMap": "aws_vpc_security_group_egress_rule.security_group_rule_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-security-group.json b/docs.overmind.tech/docs/sources/aws/data/ec2-security-group.json new file mode 100644 index 00000000..52075401 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-security-group.json @@ -0,0 +1,22 @@ +{ + "type": "ec2-security-group", + "category": 4, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "Security Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a security group by ID", + "list": true, + "listDescription": "List all security groups", + "search": true, + "searchDescription": "Search for security groups by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_security_group.id" + }, + { + "terraformQueryMap": "aws_security_group_rule.security_group_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-snapshot.json b/docs.overmind.tech/docs/sources/aws/data/ec2-snapshot.json new file mode 100644 index 00000000..fd62ef95 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-snapshot.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-snapshot", + "category": 2, + "potentialLinks": ["ec2-volume"], + "descriptiveName": "EC2 Snapshot", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a snapshot by ID", + "list": true, + "listDescription": "List all snapshots", + "search": true, + "searchDescription": "Search snapshots by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-subnet.json b/docs.overmind.tech/docs/sources/aws/data/ec2-subnet.json new file mode 100644 index 00000000..3f82850c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-subnet.json @@ -0,0 +1,22 @@ +{ + "type": "ec2-subnet", + "category": 3, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "EC2 Subnet", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a subnet by ID", + "list": true, + "listDescription": "List all subnets", + "search": true, + "searchDescription": "Search for subnets by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_route_table_association.subnet_id" + }, + { + "terraformQueryMap": "aws_subnet.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-association.json b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-association.json new file mode 100644 index 00000000..295f0267 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-association.json @@ -0,0 +1 @@ +{"type":"ec2-transit-gateway-route-table-association","category":3,"potentialLinks":["ec2-transit-gateway-route-table","ec2-transit-gateway-attachment","ec2-vpc","ec2-vpn-connection","directconnect-direct-connect-gateway"],"descriptiveName":"Transit Gateway Route Table Association","supportedQueryMethods":{"get":true,"getDescription":"Get by TransitGatewayRouteTableId|TransitGatewayAttachmentId","list":true,"listDescription":"List all route table associations","search":true,"searchDescription":"Search by TransitGatewayRouteTableId to list associations for that route table"},"terraformMappings":[{"terraformQueryMap":"aws_ec2_transit_gateway_route_table_association.id"}]} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-propagation.json b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-propagation.json new file mode 100644 index 00000000..f7f37b4d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-propagation.json @@ -0,0 +1 @@ +{"type":"ec2-transit-gateway-route-table-propagation","category":3,"potentialLinks":["ec2-transit-gateway-route-table","ec2-transit-gateway-route-table-association","ec2-transit-gateway-attachment","ec2-vpc","ec2-vpn-connection","directconnect-direct-connect-gateway"],"descriptiveName":"Transit Gateway Route Table Propagation","supportedQueryMethods":{"get":true,"getDescription":"Get by TransitGatewayRouteTableId|TransitGatewayAttachmentId","list":true,"listDescription":"List all route table propagations","search":true,"searchDescription":"Search by TransitGatewayRouteTableId to list propagations for that route table"},"terraformMappings":[{"terraformQueryMap":"aws_ec2_transit_gateway_route_table_propagation.id"}]} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table.json b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table.json new file mode 100644 index 00000000..d0dad238 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table.json @@ -0,0 +1 @@ +{"type":"ec2-transit-gateway-route-table","category":3,"potentialLinks":["ec2-transit-gateway","ec2-transit-gateway-route-table-association","ec2-transit-gateway-route-table-propagation","ec2-transit-gateway-route"],"descriptiveName":"Transit Gateway Route Table","supportedQueryMethods":{"get":true,"getDescription":"Get a transit gateway route table by ID","list":true,"listDescription":"List all transit gateway route tables","search":true,"searchDescription":"Search transit gateway route tables by ARN"},"terraformMappings":[{"terraformQueryMap":"aws_ec2_transit_gateway_route_table.id"}]} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route.json b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route.json new file mode 100644 index 00000000..01c80f1b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route.json @@ -0,0 +1 @@ +{"type":"ec2-transit-gateway-route","category":3,"potentialLinks":["ec2-transit-gateway-route-table","ec2-transit-gateway-route-table-association","ec2-transit-gateway-attachment","ec2-transit-gateway-route-table-announcement","ec2-vpc","ec2-vpn-connection","ec2-managed-prefix-list","directconnect-direct-connect-gateway"],"descriptiveName":"Transit Gateway Route","supportedQueryMethods":{"get":true,"getDescription":"Get by TransitGatewayRouteTableId|Destination (CIDR or pl:PrefixListId)","list":true,"listDescription":"List all transit gateway routes","search":true,"searchDescription":"Search by TransitGatewayRouteTableId to list routes for that route table"},"terraformMappings":[{"terraformQueryMap":"aws_ec2_transit_gateway_route.id"}]} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-volume-status.json b/docs.overmind.tech/docs/sources/aws/data/ec2-volume-status.json new file mode 100644 index 00000000..6f51fd4f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-volume-status.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-volume-status", + "category": 5, + "potentialLinks": ["ec2-instance"], + "descriptiveName": "EC2 Volume Status", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a volume status by volume ID", + "list": true, + "listDescription": "List all volume statuses", + "search": true, + "searchDescription": "Search for volume statuses by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-volume.json b/docs.overmind.tech/docs/sources/aws/data/ec2-volume.json new file mode 100644 index 00000000..1420c27c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-volume.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-volume", + "category": 2, + "potentialLinks": ["ec2-instance"], + "descriptiveName": "EC2 Volume", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a volume by ID", + "list": true, + "listDescription": "List all volumes", + "search": true, + "searchDescription": "Search volumes by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ebs_volume.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-endpoint.json b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-endpoint.json new file mode 100644 index 00000000..5c175313 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-endpoint.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-vpc-endpoint", + "category": 3, + "descriptiveName": "VPC Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a VPC Endpoint by ID", + "list": true, + "listDescription": "List all VPC Endpoints", + "search": true, + "searchDescription": "Search VPC Endpoints by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_vpc_endpoint.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-peering-connection.json b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-peering-connection.json new file mode 100644 index 00000000..6bcc6aa7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-peering-connection.json @@ -0,0 +1,25 @@ +{ + "type": "ec2-vpc-peering-connection", + "category": 3, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "VPC Peering Connection", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a VPC Peering Connection by ID", + "list": true, + "listDescription": "List all VPC Peering Connections", + "search": true, + "searchDescription": "Search for VPC Peering Connections by their ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_vpc_peering_connection.id" + }, + { + "terraformQueryMap": "aws_vpc_peering_connection_accepter.id" + }, + { + "terraformQueryMap": "aws_vpc_peering_connection_options.vpc_peering_connection_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-vpc.json b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc.json new file mode 100644 index 00000000..3df7e1e8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc.json @@ -0,0 +1,16 @@ +{ + "type": "ec2-vpc", + "category": 3, + "descriptiveName": "VPC", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a VPC by ID", + "list": true, + "listDescription": "List all VPCs" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_vpc.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-capacity-provider.json b/docs.overmind.tech/docs/sources/aws/data/ecs-capacity-provider.json new file mode 100644 index 00000000..6c44da49 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-capacity-provider.json @@ -0,0 +1,20 @@ +{ + "type": "ecs-capacity-provider", + "category": 7, + "potentialLinks": ["autoscaling-auto-scaling-group"], + "descriptiveName": "Capacity Provider", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a capacity provider by its short name or full Amazon Resource Name (ARN).", + "list": true, + "listDescription": "List capacity providers.", + "search": true, + "searchDescription": "Search capacity providers by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_ecs_capacity_provider.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-cluster.json b/docs.overmind.tech/docs/sources/aws/data/ecs-cluster.json new file mode 100644 index 00000000..968ac8ec --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-cluster.json @@ -0,0 +1,25 @@ +{ + "type": "ecs-cluster", + "category": 1, + "potentialLinks": [ + "ecs-container-instance", + "ecs-service", + "ecs-task", + "ecs-capacity-provider" + ], + "descriptiveName": "ECS Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a cluster by name", + "list": true, + "listDescription": "List all clusters", + "search": true, + "searchDescription": "Search for a cluster by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_ecs_cluster.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-container-instance.json b/docs.overmind.tech/docs/sources/aws/data/ecs-container-instance.json new file mode 100644 index 00000000..918fcd86 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-container-instance.json @@ -0,0 +1,12 @@ +{ + "type": "ecs-container-instance", + "category": 1, + "potentialLinks": ["ec2-instance"], + "descriptiveName": "Container Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a container instance by ID which consists of {clusterName}/{id}", + "search": true, + "searchDescription": "Search for container instances by cluster" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-service.json b/docs.overmind.tech/docs/sources/aws/data/ecs-service.json new file mode 100644 index 00000000..2023b25a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-service.json @@ -0,0 +1,27 @@ +{ + "type": "ecs-service", + "category": 1, + "potentialLinks": [ + "ecs-cluster", + "elbv2-target-group", + "servicediscovery-service", + "ecs-task-definition", + "ecs-capacity-provider", + "ec2-subnet", + "ecs-security-group", + "dns" + ], + "descriptiveName": "ECS Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ECS service by full name ({clusterName}/{id})", + "search": true, + "searchDescription": "Search for ECS services by cluster" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_ecs_service.cluster_name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-task-definition.json b/docs.overmind.tech/docs/sources/aws/data/ecs-task-definition.json new file mode 100644 index 00000000..76223f75 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-task-definition.json @@ -0,0 +1,19 @@ +{ + "type": "ecs-task-definition", + "category": 1, + "potentialLinks": ["iam-role", "secretsmanager-secret", "ssm-parameter"], + "descriptiveName": "Task Definition", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a task definition by revision name ({family}:{revision})", + "list": true, + "listDescription": "List all task definitions", + "search": true, + "searchDescription": "Search for task definitions by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ecs_task_definition.family" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-task.json b/docs.overmind.tech/docs/sources/aws/data/ecs-task.json new file mode 100644 index 00000000..4563d299 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-task.json @@ -0,0 +1,18 @@ +{ + "type": "ecs-task", + "category": 1, + "potentialLinks": [ + "ecs-cluster", + "ecs-container-instance", + "ecs-task-definition", + "ec2-network-interface", + "ip" + ], + "descriptiveName": "ECS Task", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ECS task by ID", + "search": true, + "searchDescription": "Search for ECS tasks by cluster" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-access-point.json b/docs.overmind.tech/docs/sources/aws/data/efs-access-point.json new file mode 100644 index 00000000..f52ca656 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-access-point.json @@ -0,0 +1,18 @@ +{ + "type": "efs-access-point", + "category": 3, + "descriptiveName": "EFS Access Point", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an access point by ID", + "list": true, + "listDescription": "List all access points", + "search": true, + "searchDescription": "Search for an access point by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_access_point.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-backup-policy.json b/docs.overmind.tech/docs/sources/aws/data/efs-backup-policy.json new file mode 100644 index 00000000..41acfabb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-backup-policy.json @@ -0,0 +1,16 @@ +{ + "type": "efs-backup-policy", + "category": 2, + "descriptiveName": "EFS Backup Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an Backup Policy by file system ID", + "search": true, + "searchDescription": "Search for an Backup Policy by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_backup_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-file-system.json b/docs.overmind.tech/docs/sources/aws/data/efs-file-system.json new file mode 100644 index 00000000..69da6ccb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-file-system.json @@ -0,0 +1,18 @@ +{ + "type": "efs-file-system", + "category": 2, + "descriptiveName": "EFS File System", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a file system by ID", + "list": true, + "listDescription": "List file systems", + "search": true, + "searchDescription": "Search file systems by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_file_system.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-mount-target.json b/docs.overmind.tech/docs/sources/aws/data/efs-mount-target.json new file mode 100644 index 00000000..ae47e2a4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-mount-target.json @@ -0,0 +1,16 @@ +{ + "type": "efs-mount-target", + "category": 2, + "descriptiveName": "EFS Mount Target", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an mount target by ID", + "search": true, + "searchDescription": "Search for mount targets by file system ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_mount_target.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-replication-configuration.json b/docs.overmind.tech/docs/sources/aws/data/efs-replication-configuration.json new file mode 100644 index 00000000..9d790dba --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-replication-configuration.json @@ -0,0 +1,18 @@ +{ + "type": "efs-replication-configuration", + "category": 2, + "descriptiveName": "EFS Replication Configuration", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a replication configuration by file system ID", + "list": true, + "listDescription": "List all replication configurations", + "search": true, + "searchDescription": "Search for a replication configuration by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_replication_configuration.source_file_system_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/eks-addon.json b/docs.overmind.tech/docs/sources/aws/data/eks-addon.json new file mode 100644 index 00000000..f453ac10 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/eks-addon.json @@ -0,0 +1,16 @@ +{ + "type": "eks-addon", + "category": 1, + "descriptiveName": "EKS Addon", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an addon by unique name ({clusterName}:{addonName})", + "search": true, + "searchDescription": "Search addons by cluster name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_eks_addon.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/eks-cluster.json b/docs.overmind.tech/docs/sources/aws/data/eks-cluster.json new file mode 100644 index 00000000..e59349e5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/eks-cluster.json @@ -0,0 +1,19 @@ +{ + "type": "eks-cluster", + "category": 1, + "descriptiveName": "EKS Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a cluster by name", + "list": true, + "listDescription": "List all clusters", + "search": true, + "searchDescription": "Search for clusters by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_eks_cluster.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/eks-fargate-profile.json b/docs.overmind.tech/docs/sources/aws/data/eks-fargate-profile.json new file mode 100644 index 00000000..29112a7b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/eks-fargate-profile.json @@ -0,0 +1,17 @@ +{ + "type": "eks-fargate-profile", + "category": 7, + "potentialLinks": ["iam-role", "ec2-subnet"], + "descriptiveName": "Fargate Profile", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a fargate profile by unique name ({clusterName}:{FargateProfileName})", + "search": true, + "searchDescription": "Search for fargate profiles by cluster name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_eks_fargate_profile.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/eks-nodegroup.json b/docs.overmind.tech/docs/sources/aws/data/eks-nodegroup.json new file mode 100644 index 00000000..514116c5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/eks-nodegroup.json @@ -0,0 +1,23 @@ +{ + "type": "eks-nodegroup", + "category": 1, + "potentialLinks": [ + "ec2-key-pair", + "ec2-security-group", + "ec2-subnet", + "autoscaling-auto-scaling-group", + "ec2-launch-template" + ], + "descriptiveName": "EKS Nodegroup", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a node group by unique name ({clusterName}:{NodegroupName})", + "search": true, + "searchDescription": "Search for node groups by cluster name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_eks_node_group.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elb-instance-health.json b/docs.overmind.tech/docs/sources/aws/data/elb-instance-health.json new file mode 100644 index 00000000..f38a9830 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elb-instance-health.json @@ -0,0 +1,12 @@ +{ + "type": "elb-instance-health", + "category": 5, + "potentialLinks": ["ec2-instance"], + "descriptiveName": "ELB Instance Health", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get instance health by ID ({LoadBalancerName}/{InstanceId})", + "list": true, + "listDescription": "List all instance healths" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elb-load-balancer.json b/docs.overmind.tech/docs/sources/aws/data/elb-load-balancer.json new file mode 100644 index 00000000..bbd6805a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elb-load-balancer.json @@ -0,0 +1,28 @@ +{ + "type": "elb-load-balancer", + "category": 3, + "potentialLinks": [ + "dns", + "route53-hosted-zone", + "ec2-subnet", + "ec2-vpc", + "ec2-instance", + "elb-instance-health", + "ec2-security-group" + ], + "descriptiveName": "Classic Load Balancer", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a classic load balancer by name", + "list": true, + "listDescription": "List all classic load balancers", + "search": true, + "searchDescription": "Search for classic load balancers by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_elb.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-listener.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-listener.json new file mode 100644 index 00000000..5ae2f13c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-listener.json @@ -0,0 +1,27 @@ +{ + "type": "elbv2-listener", + "category": 3, + "potentialLinks": [ + "elbv2-load-balancer", + "acm-certificate", + "elbv2-rule", + "cognito-idp-user-pool", + "http", + "elbv2-target-group" + ], + "descriptiveName": "ELB Listener", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ELB listener by ARN", + "search": true, + "searchDescription": "Search for ELB listeners by load balancer ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_alb_listener.arn" + }, + { + "terraformQueryMap": "aws_lb_listener.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-load-balancer.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-load-balancer.json new file mode 100644 index 00000000..a43cb811 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-load-balancer.json @@ -0,0 +1,34 @@ +{ + "type": "elbv2-load-balancer", + "category": 3, + "potentialLinks": [ + "elbv2-target-group", + "elbv2-listener", + "dns", + "route53-hosted-zone", + "ec2-vpc", + "ec2-subnet", + "ec2-address", + "ip", + "ec2-security-group", + "ec2-coip-pool" + ], + "descriptiveName": "Elastic Load Balancer", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ELB by name", + "list": true, + "listDescription": "List all ELBs", + "search": true, + "searchDescription": "Search for ELBs by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_lb.arn" + }, + { + "terraformQueryMap": "aws_lb.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-rule.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-rule.json new file mode 100644 index 00000000..02271e06 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-rule.json @@ -0,0 +1,19 @@ +{ + "type": "elbv2-rule", + "category": 7, + "descriptiveName": "ELB Rule", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a rule by ARN", + "search": true, + "searchDescription": "Search for rules by listener ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_alb_listener_rule.arn" + }, + { + "terraformQueryMap": "aws_lb_listener_rule.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-target-group.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-target-group.json new file mode 100644 index 00000000..d98f3986 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-target-group.json @@ -0,0 +1,24 @@ +{ + "type": "elbv2-target-group", + "category": 3, + "potentialLinks": ["ec2-vpc", "elbv2-load-balancer", "elbv2-target-health"], + "descriptiveName": "Target Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a target group by name", + "list": true, + "listDescription": "List all target groups", + "search": true, + "searchDescription": "Search for target groups by load balancer ARN or target group ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_alb_target_group.arn" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_lb_target_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-target-health.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-target-health.json new file mode 100644 index 00000000..5b56610e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-target-health.json @@ -0,0 +1,17 @@ +{ + "type": "elbv2-target-health", + "category": 5, + "potentialLinks": [ + "ec2-instance", + "lambda-function", + "ip", + "elbv2-load-balancer" + ], + "descriptiveName": "ELB Target Health", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get target health by unique ID ({TargetGroupArn}|{Id}|{AvailabilityZone}|{Port})", + "search": true, + "searchDescription": "Search for target health by target group ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-group.json b/docs.overmind.tech/docs/sources/aws/data/iam-group.json new file mode 100644 index 00000000..b29e1eee --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-group.json @@ -0,0 +1,19 @@ +{ + "type": "iam-group", + "category": 4, + "descriptiveName": "IAM Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a group by name", + "list": true, + "listDescription": "List all IAM groups", + "search": true, + "searchDescription": "Search for a group by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-instance-profile.json b/docs.overmind.tech/docs/sources/aws/data/iam-instance-profile.json new file mode 100644 index 00000000..baf7d26c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-instance-profile.json @@ -0,0 +1,20 @@ +{ + "type": "iam-instance-profile", + "category": 4, + "potentialLinks": ["iam-role", "iam-policy"], + "descriptiveName": "IAM Instance Profile", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM instance profile by name", + "list": true, + "listDescription": "List all IAM instance profiles", + "search": true, + "searchDescription": "Search IAM instance profiles by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_instance_profile.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-policy.json b/docs.overmind.tech/docs/sources/aws/data/iam-policy.json new file mode 100644 index 00000000..45065c35 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-policy.json @@ -0,0 +1,24 @@ +{ + "type": "iam-policy", + "category": 4, + "potentialLinks": ["iam-group", "iam-user", "iam-role"], + "descriptiveName": "IAM Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM policy by policyFullName ({path} + {policyName})", + "list": true, + "listDescription": "List all IAM policies", + "search": true, + "searchDescription": "Search for IAM policies by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_policy.arn" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_user_policy_attachment.policy_arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-role.json b/docs.overmind.tech/docs/sources/aws/data/iam-role.json new file mode 100644 index 00000000..055312c0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-role.json @@ -0,0 +1,20 @@ +{ + "type": "iam-role", + "category": 4, + "potentialLinks": ["iam-policy"], + "descriptiveName": "IAM Role", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM role by name", + "list": true, + "listDescription": "List all IAM roles", + "search": true, + "searchDescription": "Search for IAM roles by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_role.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-user.json b/docs.overmind.tech/docs/sources/aws/data/iam-user.json new file mode 100644 index 00000000..ea0a8ab1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-user.json @@ -0,0 +1,23 @@ +{ + "type": "iam-user", + "category": 4, + "potentialLinks": ["iam-group"], + "descriptiveName": "IAM User", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM user by name", + "list": true, + "listDescription": "List all IAM users", + "search": true, + "searchDescription": "Search for IAM users by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_user.arn" + }, + { + "terraformQueryMap": "aws_iam_user_group_membership.user" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-alias.json b/docs.overmind.tech/docs/sources/aws/data/kms-alias.json new file mode 100644 index 00000000..734fede8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-alias.json @@ -0,0 +1,19 @@ +{ + "type": "kms-alias", + "category": 4, + "potentialLinks": ["kms-key"], + "descriptiveName": "KMS Alias", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an alias by keyID/aliasName", + "list": true, + "listDescription": "List all aliases", + "search": true, + "searchDescription": "Search aliases by keyID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_alias.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-custom-key-store.json b/docs.overmind.tech/docs/sources/aws/data/kms-custom-key-store.json new file mode 100644 index 00000000..76550d54 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-custom-key-store.json @@ -0,0 +1,19 @@ +{ + "type": "kms-custom-key-store", + "category": 2, + "potentialLinks": ["cloudhsmv2-cluster", "ec2-vpc-endpoint-service"], + "descriptiveName": "Custom Key Store", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a custom key store by its ID", + "list": true, + "listDescription": "List all custom key stores", + "search": true, + "searchDescription": "Search custom key store by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_custom_key_store.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-grant.json b/docs.overmind.tech/docs/sources/aws/data/kms-grant.json new file mode 100644 index 00000000..0e29bf70 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-grant.json @@ -0,0 +1,17 @@ +{ + "type": "kms-grant", + "category": 4, + "potentialLinks": ["kms-key", "iam-user", "iam-role"], + "descriptiveName": "KMS Grant", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a grant by keyID/grantId", + "search": true, + "searchDescription": "Search grants by keyID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_grant.grant_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-key-policy.json b/docs.overmind.tech/docs/sources/aws/data/kms-key-policy.json new file mode 100644 index 00000000..74452c63 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-key-policy.json @@ -0,0 +1,17 @@ +{ + "type": "kms-key-policy", + "category": 4, + "potentialLinks": ["kms-key"], + "descriptiveName": "KMS Key Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a KMS key policy by its Key ID", + "search": true, + "searchDescription": "Search KMS key policies by Key ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_key_policy.key_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-key.json b/docs.overmind.tech/docs/sources/aws/data/kms-key.json new file mode 100644 index 00000000..a292b5f9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-key.json @@ -0,0 +1,19 @@ +{ + "type": "kms-key", + "category": 4, + "potentialLinks": ["kms-custom-key-store", "kms-key-policy", "kms-grant"], + "descriptiveName": "KMS Key", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a KMS Key by its ID", + "list": true, + "listDescription": "List all KMS Keys", + "search": true, + "searchDescription": "Search for KMS Keys by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_key.key_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/lambda-event-source-mapping.json b/docs.overmind.tech/docs/sources/aws/data/lambda-event-source-mapping.json new file mode 100644 index 00000000..ecf68cb5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/lambda-event-source-mapping.json @@ -0,0 +1,28 @@ +{ + "type": "lambda-event-source-mapping", + "category": 1, + "potentialLinks": [ + "lambda-function", + "dynamodb-table", + "kinesis-stream", + "sqs-queue", + "kafka-cluster", + "mq-broker", + "rds-db-cluster" + ], + "descriptiveName": "Lambda Event Source Mapping", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Lambda event source mapping by UUID", + "list": true, + "listDescription": "List all Lambda event source mappings", + "search": true, + "searchDescription": "Search for Lambda event source mappings by Event Source ARN (SQS, DynamoDB, Kinesis, etc.)" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_lambda_event_source_mapping.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/lambda-function.json b/docs.overmind.tech/docs/sources/aws/data/lambda-function.json new file mode 100644 index 00000000..2c0e8ca8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/lambda-function.json @@ -0,0 +1,35 @@ +{ + "type": "lambda-function", + "category": 1, + "potentialLinks": [ + "iam-role", + "s3-bucket", + "sns-topic", + "sqs-queue", + "lambda-function", + "events-event-bus", + "elbv2-target-group", + "vpc-lattice-target-group", + "logs-log-group" + ], + "descriptiveName": "Lambda Function", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a lambda function by name", + "list": true, + "listDescription": "List all lambda functions", + "search": true, + "searchDescription": "Search for lambda functions by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_lambda_function.arn" + }, + { + "terraformQueryMap": "aws_lambda_function_event_invoke_config.id" + }, + { + "terraformQueryMap": "aws_lambda_function_url.function_arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/lambda-layer-version.json b/docs.overmind.tech/docs/sources/aws/data/lambda-layer-version.json new file mode 100644 index 00000000..71ba69ab --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/lambda-layer-version.json @@ -0,0 +1,17 @@ +{ + "type": "lambda-layer-version", + "category": 1, + "potentialLinks": ["signer-signing-job", "signer-signing-profile"], + "descriptiveName": "Lambda Layer Version", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a layer version by full name ({layerName}:{versionNumber})", + "search": true, + "searchDescription": "Search for layer versions by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_lambda_layer_version.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/lambda-layer.json b/docs.overmind.tech/docs/sources/aws/data/lambda-layer.json new file mode 100644 index 00000000..9be5fdb8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/lambda-layer.json @@ -0,0 +1,10 @@ +{ + "type": "lambda-layer", + "category": 1, + "potentialLinks": ["lambda-layer-version"], + "descriptiveName": "Lambda Layer", + "supportedQueryMethods": { + "list": true, + "listDescription": "List all lambda layers" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall-policy.json b/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall-policy.json new file mode 100644 index 00000000..783c3d5d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall-policy.json @@ -0,0 +1,23 @@ +{ + "type": "network-firewall-firewall-policy", + "category": 3, + "potentialLinks": [ + "network-firewall-rule-group", + "network-firewall-tls-inspection-configuration", + "kms-key" + ], + "descriptiveName": "Network Firewall Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Network Firewall Policy by name", + "list": true, + "listDescription": "List Network Firewall Policies", + "search": true, + "searchDescription": "Search for Network Firewall Policies by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkfirewall_firewall_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall.json b/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall.json new file mode 100644 index 00000000..c140fdfc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall.json @@ -0,0 +1,28 @@ +{ + "type": "network-firewall-firewall", + "category": 3, + "potentialLinks": [ + "network-firewall-firewall-policy", + "ec2-subnet", + "ec2-vpc", + "logs-log-group", + "s3-bucket", + "firehose-delivery-stream", + "iam-policy", + "kms-key" + ], + "descriptiveName": "Network Firewall", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Network Firewall by name", + "list": true, + "listDescription": "List Network Firewalls", + "search": true, + "searchDescription": "Search for Network Firewalls by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkfirewall_firewall.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/network-firewall-rule-group.json b/docs.overmind.tech/docs/sources/aws/data/network-firewall-rule-group.json new file mode 100644 index 00000000..74559d23 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/network-firewall-rule-group.json @@ -0,0 +1,19 @@ +{ + "type": "network-firewall-rule-group", + "category": 4, + "potentialLinks": ["kms-key", "sns-topic", "network-firewall-rule-group"], + "descriptiveName": "Network Firewall Rule Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Network Firewall Rule Group by name", + "list": true, + "listDescription": "List Network Firewall Rule Groups", + "search": true, + "searchDescription": "Search for Network Firewall Rule Groups by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkfirewall_rule_group.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/network-firewall-tls-inspection-configuration.json b/docs.overmind.tech/docs/sources/aws/data/network-firewall-tls-inspection-configuration.json new file mode 100644 index 00000000..fa09ef24 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/network-firewall-tls-inspection-configuration.json @@ -0,0 +1,19 @@ +{ + "type": "network-firewall-tls-inspection-configuration", + "category": 7, + "potentialLinks": [ + "acm-certificate", + "acm-pca-certificate-authority", + "acm-pca-certificate-authority-certificate", + "network-firewall-encryption-configuration" + ], + "descriptiveName": "Network Firewall TLS Inspection Configuration", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Network Firewall TLS Inspection Configuration by name", + "list": true, + "listDescription": "List Network Firewall TLS Inspection Configurations", + "search": true, + "searchDescription": "Search for Network Firewall TLS Inspection Configurations by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-attachment.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-attachment.json new file mode 100644 index 00000000..b25960d6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-attachment.json @@ -0,0 +1,14 @@ +{ + "type": "networkmanager-connect-attachment", + "category": 3, + "potentialLinks": ["networkmanager-core-network"], + "descriptiveName": "Networkmanager Connect Attachment", + "supportedQueryMethods": { + "get": true + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_core_network.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer-association.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer-association.json new file mode 100644 index 00000000..439f70bc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer-association.json @@ -0,0 +1,19 @@ +{ + "type": "networkmanager-connect-peer-association", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-connect-peer", + "networkmanager-device", + "networkmanager-link" + ], + "descriptiveName": "Networkmanager Connect Peer Association", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Connect Peer Association", + "list": true, + "listDescription": "List all Networkmanager Connect Peer Associations", + "search": true, + "searchDescription": "Search for Networkmanager ConnectPeerAssociations by GlobalNetworkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer.json new file mode 100644 index 00000000..9b92d159 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer.json @@ -0,0 +1,21 @@ +{ + "type": "networkmanager-connect-peer", + "category": 3, + "potentialLinks": [ + "networkmanager-core-network", + "networkmanager-connect-attachment", + "ip", + "rdap-asn", + "ec2-subnet" + ], + "descriptiveName": "Networkmanager Connect Peer", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Connect Peer by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_connect_peer.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-connection.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connection.json new file mode 100644 index 00000000..f8e45be7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connection.json @@ -0,0 +1,22 @@ +{ + "type": "networkmanager-connection", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-link", + "networkmanager-device" + ], + "descriptiveName": "Networkmanager Connection", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Connection", + "search": true, + "searchDescription": "Search for Networkmanager Connections by GlobalNetworkId, Device ARN, or Connection ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_connection.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network-policy.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network-policy.json new file mode 100644 index 00000000..922a72cb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network-policy.json @@ -0,0 +1,15 @@ +{ + "type": "networkmanager-core-network-policy", + "category": 3, + "potentialLinks": ["networkmanager-core-network"], + "descriptiveName": "Networkmanager Core Network Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Core Network Policy by Core Network id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_core_network_policy.core_network_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network.json new file mode 100644 index 00000000..701eb4f6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network.json @@ -0,0 +1,18 @@ +{ + "type": "networkmanager-core-network", + "category": 3, + "potentialLinks": [ + "networkmanager-core-network-policy", + "networkmanager-connect-peer" + ], + "descriptiveName": "Networkmanager Core Network", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Core Network by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_core_network.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-device.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-device.json new file mode 100644 index 00000000..8a83b60c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-device.json @@ -0,0 +1,24 @@ +{ + "type": "networkmanager-device", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-site", + "networkmanager-link-association", + "networkmanager-connection", + "networkmanager-network-resource-relationship" + ], + "descriptiveName": "Networkmanager Device", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Device", + "search": true, + "searchDescription": "Search for Networkmanager Devices by GlobalNetworkId, {GlobalNetworkId|SiteId} or ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_device.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-global-network.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-global-network.json new file mode 100644 index 00000000..d699bb33 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-global-network.json @@ -0,0 +1,29 @@ +{ + "type": "networkmanager-global-network", + "category": 3, + "potentialLinks": [ + "networkmanager-site", + "networkmanager-transit-gateway-registration", + "networkmanager-connect-peer-association", + "networkmanager-transit-gateway-connect-peer-association", + "networkmanager-network-resource-relationship", + "networkmanager-link", + "networkmanager-device", + "networkmanager-connection" + ], + "descriptiveName": "Network Manager Global Network", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a global network by id", + "list": true, + "listDescription": "List all global networks", + "search": true, + "searchDescription": "Search for a global network by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_global_network.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-link-association.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-link-association.json new file mode 100644 index 00000000..4a80e3fe --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-link-association.json @@ -0,0 +1,16 @@ +{ + "type": "networkmanager-link-association", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-link", + "networkmanager-device" + ], + "descriptiveName": "Networkmanager LinkAssociation", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Link Association", + "search": true, + "searchDescription": "Search for Networkmanager Link Associations by GlobalNetworkId and DeviceId or LinkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-link.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-link.json new file mode 100644 index 00000000..950eeb0e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-link.json @@ -0,0 +1,23 @@ +{ + "type": "networkmanager-link", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-link-association", + "networkmanager-site", + "networkmanager-network-resource-relationship" + ], + "descriptiveName": "Networkmanager Link", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Link", + "search": true, + "searchDescription": "Search for Networkmanager Links by GlobalNetworkId, GlobalNetworkId with SiteId, or ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_link.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-network-resource-relationship.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-network-resource-relationship.json new file mode 100644 index 00000000..b98c6c14 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-network-resource-relationship.json @@ -0,0 +1,19 @@ +{ + "type": "networkmanager-network-resource-relationship", + "category": 3, + "potentialLinks": [ + "networkmanager-connection", + "networkmanager-device", + "networkmanager-link", + "networkmanager-site", + "directconnect-connection", + "directconnect-direct-connect-gateway", + "directconnect-virtual-interface", + "ec2-customer" + ], + "descriptiveName": "Networkmanager Network Resource Relationships", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Search for Networkmanager NetworkResourceRelationships by GlobalNetworkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-site-to-site-vpn-attachment.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-site-to-site-vpn-attachment.json new file mode 100644 index 00000000..78a07ca5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-site-to-site-vpn-attachment.json @@ -0,0 +1,15 @@ +{ + "type": "networkmanager-site-to-site-vpn-attachment", + "category": 3, + "potentialLinks": ["networkmanager-core-network", "ec2-vpn-connection"], + "descriptiveName": "Networkmanager Site To Site Vpn Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Site To Site Vpn Attachment by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_site_to_site_vpn_attachment.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-site.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-site.json new file mode 100644 index 00000000..e34cb165 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-site.json @@ -0,0 +1,22 @@ +{ + "type": "networkmanager-site", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-link", + "networkmanager-device" + ], + "descriptiveName": "Networkmanager Site", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Site", + "search": true, + "searchDescription": "Search for Networkmanager Sites by GlobalNetworkId or Site ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_site.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-connect-peer-association.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-connect-peer-association.json new file mode 100644 index 00000000..dc8fa0ca --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-connect-peer-association.json @@ -0,0 +1,18 @@ +{ + "type": "networkmanager-transit-gateway-connect-peer-association", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-device", + "networkmanager-link" + ], + "descriptiveName": "Networkmanager Transit Gateway Connect Peer Association", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Transit Gateway Connect Peer Association by id", + "list": true, + "listDescription": "List all Networkmanager Transit Gateway Connect Peer Associations", + "search": true, + "searchDescription": "Search for Networkmanager Transit Gateway Connect Peer Associations by GlobalNetworkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-peering.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-peering.json new file mode 100644 index 00000000..1790a705 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-peering.json @@ -0,0 +1,19 @@ +{ + "type": "networkmanager-transit-gateway-peering", + "category": 3, + "potentialLinks": [ + "networkmanager-core-network", + "ec2-transit-gateway-peering-attachment", + "ec2-transit-gateway" + ], + "descriptiveName": "Networkmanager Transit Gateway Peering", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Transit Gateway Peering by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_transit_gateway_peering.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-registration.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-registration.json new file mode 100644 index 00000000..9f38242b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-registration.json @@ -0,0 +1,14 @@ +{ + "type": "networkmanager-transit-gateway-registration", + "category": 3, + "potentialLinks": ["networkmanager-global-network", "ec2-transit-gateway"], + "descriptiveName": "Networkmanager Transit Gateway Registrations", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Transit Gateway Registrations", + "list": true, + "listDescription": "List all Networkmanager Transit Gateway Registrations", + "search": true, + "searchDescription": "Search for Networkmanager Transit Gateway Registrations by GlobalNetworkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-route-table-attachment.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-route-table-attachment.json new file mode 100644 index 00000000..a07ef503 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-route-table-attachment.json @@ -0,0 +1,19 @@ +{ + "type": "networkmanager-transit-gateway-route-table-attachment", + "category": 3, + "potentialLinks": [ + "networkmanager-core-network", + "networkmanager-transit-gateway-peering", + "ec2-transit-gateway-route-table" + ], + "descriptiveName": "Networkmanager Transit Gateway Route Table Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Transit Gateway Route Table Attachment by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_transit_gateway_route_table_attachment.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-vpc-attachment.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-vpc-attachment.json new file mode 100644 index 00000000..c7ad8625 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-vpc-attachment.json @@ -0,0 +1,15 @@ +{ + "type": "networkmanager-vpc-attachment", + "category": 3, + "potentialLinks": ["networkmanager-core-network"], + "descriptiveName": "Networkmanager VPC Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager VPC Attachment by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_vpc_attachment.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster-parameter-group.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster-parameter-group.json new file mode 100644 index 00000000..31c354f1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster-parameter-group.json @@ -0,0 +1,19 @@ +{ + "type": "rds-db-cluster-parameter-group", + "category": 6, + "descriptiveName": "RDS Cluster Parameter Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a parameter group by name", + "list": true, + "listDescription": "List all RDS parameter groups", + "search": true, + "searchDescription": "Search for a parameter group by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_rds_cluster_parameter_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster.json new file mode 100644 index 00000000..6c88f5fe --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster.json @@ -0,0 +1,30 @@ +{ + "type": "rds-db-cluster", + "category": 6, + "potentialLinks": [ + "rds-db-subnet-group", + "dns", + "rds-db-cluster", + "ec2-security-group", + "route53-hosted-zone", + "kms-key", + "kinesis-stream", + "rds-option-group", + "secretsmanager-secret", + "iam-role" + ], + "descriptiveName": "RDS Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a parameter group by name", + "list": true, + "listDescription": "List all RDS parameter groups", + "search": true, + "searchDescription": "Search for a parameter group by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_rds_cluster.cluster_identifier" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-instance.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-instance.json new file mode 100644 index 00000000..0fd7c367 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-instance.json @@ -0,0 +1,37 @@ +{ + "type": "rds-db-instance", + "category": 6, + "potentialLinks": [ + "dns", + "route53-hosted-zone", + "ec2-security-group", + "rds-db-parameter-group", + "rds-db-subnet-group", + "rds-db-cluster", + "kms-key", + "logs-log-stream", + "iam-role", + "kinesis-stream", + "backup-recovery-point", + "iam-instance-profile", + "rds-db-instance-automated-backup", + "secretsmanager-secret" + ], + "descriptiveName": "RDS Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an instance by ID", + "list": true, + "listDescription": "List all instances", + "search": true, + "searchDescription": "Search for instances by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_db_instance.identifier" + }, + { + "terraformQueryMap": "aws_db_instance_role_association.db_instance_identifier" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-parameter-group.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-parameter-group.json new file mode 100644 index 00000000..f386e986 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-parameter-group.json @@ -0,0 +1,19 @@ +{ + "type": "rds-db-parameter-group", + "category": 6, + "descriptiveName": "RDS Parameter Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a parameter group by name", + "list": true, + "listDescription": "List all parameter groups", + "search": true, + "searchDescription": "Search for a parameter group by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_db_parameter_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-subnet-group.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-subnet-group.json new file mode 100644 index 00000000..80a46185 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-subnet-group.json @@ -0,0 +1,20 @@ +{ + "type": "rds-db-subnet-group", + "category": 3, + "potentialLinks": ["ec2-vpc", "ec2-subnet", "outposts-outpost"], + "descriptiveName": "RDS Subnet Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a subnet group by name", + "list": true, + "listDescription": "List all subnet groups", + "search": true, + "searchDescription": "Search for subnet groups by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_db_subnet_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-option-group.json b/docs.overmind.tech/docs/sources/aws/data/rds-option-group.json new file mode 100644 index 00000000..e31b8431 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-option-group.json @@ -0,0 +1,19 @@ +{ + "type": "rds-option-group", + "category": 6, + "descriptiveName": "RDS Option Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an option group by name", + "list": true, + "listDescription": "List all RDS option groups", + "search": true, + "searchDescription": "Search for an option group by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_db_option_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/route53-health-check.json b/docs.overmind.tech/docs/sources/aws/data/route53-health-check.json new file mode 100644 index 00000000..a6a6f9d0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/route53-health-check.json @@ -0,0 +1,19 @@ +{ + "type": "route53-health-check", + "category": 5, + "potentialLinks": ["cloudwatch-alarm"], + "descriptiveName": "Route53 Health Check", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get health check by ID", + "list": true, + "listDescription": "List all health checks", + "search": true, + "searchDescription": "Search for health checks by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_route53_health_check.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/route53-hosted-zone.json b/docs.overmind.tech/docs/sources/aws/data/route53-hosted-zone.json new file mode 100644 index 00000000..d9d89b61 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/route53-hosted-zone.json @@ -0,0 +1,25 @@ +{ + "type": "route53-hosted-zone", + "category": 3, + "potentialLinks": ["route53-resource-record-set"], + "descriptiveName": "Hosted Zone", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a hosted zone by ID", + "list": true, + "listDescription": "List all hosted zones", + "search": true, + "searchDescription": "Search for a hosted zone by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_route53_hosted_zone_dnssec.id" + }, + { + "terraformQueryMap": "aws_route53_zone.zone_id" + }, + { + "terraformQueryMap": "aws_route53_zone_association.zone_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/route53-resource-record-set.json b/docs.overmind.tech/docs/sources/aws/data/route53-resource-record-set.json new file mode 100644 index 00000000..7243d98a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/route53-resource-record-set.json @@ -0,0 +1,22 @@ +{ + "type": "route53-resource-record-set", + "category": 3, + "potentialLinks": ["dns", "route53-health-check"], + "descriptiveName": "Route53 Record Set", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Route53 record Set by name", + "search": true, + "searchDescription": "Search for a record set by hosted zone ID in the format \"/hostedzone/JJN928734JH7HV\" or \"JJN928734JH7HV\" or by terraform ID in the format \"{hostedZone}_{recordName}_{type}\"" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_route53_record.arn" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_route53_record.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/s3-bucket.json b/docs.overmind.tech/docs/sources/aws/data/s3-bucket.json new file mode 100644 index 00000000..cb01e849 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/s3-bucket.json @@ -0,0 +1,82 @@ +{ + "type": "s3-bucket", + "category": 2, + "potentialLinks": ["lambda-function", "sqs-queue", "sns-topic", "s3-bucket"], + "descriptiveName": "S3 Bucket", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an S3 bucket by name", + "list": true, + "listDescription": "List all S3 buckets", + "search": true, + "searchDescription": "Search for S3 buckets by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_s3_bucket_acl.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_analytics_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_cors_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_intelligent_tiering_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_inventory.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_lifecycle_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_logging.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_metric.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_notification.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_object_lock_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_object.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_ownership_controls.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_policy.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_public_access_block.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_replication_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_request_payment_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_server_side_encryption_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_versioning.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_website_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket.id" + }, + { + "terraformQueryMap": "aws_s3_object_copy.bucket" + }, + { + "terraformQueryMap": "aws_s3_object.bucket" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-data-protection-policy.json b/docs.overmind.tech/docs/sources/aws/data/sns-data-protection-policy.json new file mode 100644 index 00000000..18221376 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-data-protection-policy.json @@ -0,0 +1,17 @@ +{ + "type": "sns-data-protection-policy", + "category": 7, + "potentialLinks": ["sns-topic"], + "descriptiveName": "SNS Data Protection Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS data protection policy by associated topic ARN", + "search": true, + "searchDescription": "Search SNS data protection policies by its ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sns_topic_data_protection_policy.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-endpoint.json b/docs.overmind.tech/docs/sources/aws/data/sns-endpoint.json new file mode 100644 index 00000000..eb5b8c4f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-endpoint.json @@ -0,0 +1,11 @@ +{ + "type": "sns-endpoint", + "category": 7, + "descriptiveName": "SNS Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS endpoint by its ARN", + "search": true, + "searchDescription": "Search SNS endpoints by associated Platform Application ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-platform-application.json b/docs.overmind.tech/docs/sources/aws/data/sns-platform-application.json new file mode 100644 index 00000000..cb45be1e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-platform-application.json @@ -0,0 +1,19 @@ +{ + "type": "sns-platform-application", + "category": 7, + "potentialLinks": ["sns-endpoint"], + "descriptiveName": "SNS Platform Application", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS platform application by its ARN", + "list": true, + "listDescription": "List all SNS platform applications", + "search": true, + "searchDescription": "Search SNS platform applications by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sns_platform_application.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-subscription.json b/docs.overmind.tech/docs/sources/aws/data/sns-subscription.json new file mode 100644 index 00000000..fb347d40 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-subscription.json @@ -0,0 +1,19 @@ +{ + "type": "sns-subscription", + "category": 7, + "potentialLinks": ["sns-topic", "iam-role"], + "descriptiveName": "SNS Subscription", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS subscription by its ARN", + "list": true, + "listDescription": "List all SNS subscriptions", + "search": true, + "searchDescription": "Search SNS subscription by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sns_topic_subscription.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-topic.json b/docs.overmind.tech/docs/sources/aws/data/sns-topic.json new file mode 100644 index 00000000..ae9e460b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-topic.json @@ -0,0 +1,19 @@ +{ + "type": "sns-topic", + "category": 7, + "potentialLinks": ["kms-key"], + "descriptiveName": "SNS Topic", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS topic by its ARN", + "list": true, + "listDescription": "List all SNS topics", + "search": true, + "searchDescription": "Search SNS topic by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sns_topic.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sqs-queue.json b/docs.overmind.tech/docs/sources/aws/data/sqs-queue.json new file mode 100644 index 00000000..f21fdf16 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sqs-queue.json @@ -0,0 +1,19 @@ +{ + "type": "sqs-queue", + "category": 1, + "potentialLinks": ["http", "lambda-event-source-mapping"], + "descriptiveName": "SQS Queue", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SQS queue attributes by its URL", + "list": true, + "listDescription": "List all SQS queue URLs", + "search": true, + "searchDescription": "Search SQS queue by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sqs_queue.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ssm-parameter.json b/docs.overmind.tech/docs/sources/aws/data/ssm-parameter.json new file mode 100644 index 00000000..5d71df18 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ssm-parameter.json @@ -0,0 +1,23 @@ +{ + "type": "ssm-parameter", + "category": 7, + "potentialLinks": ["ip", "http", "dns"], + "descriptiveName": "SSM Parameter", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SSM parameter by name", + "list": true, + "listDescription": "List all SSM parameters", + "search": true, + "searchDescription": "Search for SSM parameters by ARN. This supports ARNs from IAM policies that contain wildcards" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ssm_parameter.name" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_ssm_parameter.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/terraform.md b/docs.overmind.tech/docs/sources/aws/terraform.md new file mode 100644 index 00000000..e976c988 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/terraform.md @@ -0,0 +1,182 @@ +--- +title: Configure with Terraform +sidebar_position: 2 +--- + +The [Overmind Terraform module](https://registry.terraform.io/modules/overmindtech/aws-source/overmind) configures an AWS account for Overmind infrastructure discovery in a single `terraform apply`. It creates an IAM role with a read-only policy, sets up the trust relationship, and registers the source with Overmind's API. The module is fully compatible with [OpenTofu](https://opentofu.org/). + +## Prerequisites + +- **Overmind API key** with `sources:write` scope. Create one in [Settings > API Keys](https://app.overmind.tech/settings/api-keys). +- **AWS credentials** with permission to create IAM roles and policies in the target account. +- **Terraform >= 1.5.0** or **OpenTofu >= 1.6.0**. + +## Quick Start + +```hcl +provider "overmind" {} + +provider "aws" { + region = "us-east-1" +} + +module "overmind_aws_source" { + source = "overmindtech/aws-source/overmind" + + name = "production" +} + +output "role_arn" { + value = module.overmind_aws_source.role_arn +} + +output "source_id" { + value = module.overmind_aws_source.source_id +} +``` + +Then run: + +```bash +export OVERMIND_API_KEY="your-api-key" +terraform init +terraform plan +terraform apply +``` + +## Authentication + +### Overmind Provider + +The Overmind provider reads `OVERMIND_API_KEY` from the environment. The API key must have `sources:write` scope. + +You can also set it in the provider block: + +```hcl +provider "overmind" { + api_key = var.overmind_api_key +} +``` + +### AWS Provider + +The AWS provider must have permissions to create IAM roles and policies in the target account. Any standard AWS authentication method works (environment variables, shared credentials file, SSO, etc.). See the [AWS provider documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration) for details. + +## Multi-Account Setup + +Use AWS provider aliases to onboard several accounts at once: + +```hcl +provider "overmind" {} + +provider "aws" { + alias = "production" + region = "us-east-1" + + assume_role { + role_arn = "arn:aws:iam::111111111111:role/terraform" + } +} + +provider "aws" { + alias = "staging" + region = "eu-west-1" + + assume_role { + role_arn = "arn:aws:iam::222222222222:role/terraform" + } +} + +module "overmind_production" { + source = "overmindtech/aws-source/overmind" + name = "production" + + providers = { + aws = aws.production + overmind = overmind + } +} + +module "overmind_staging" { + source = "overmindtech/aws-source/overmind" + name = "staging" + regions = ["eu-west-1"] + + providers = { + aws = aws.staging + overmind = overmind + } +} +``` + +## Inputs + +| Name | Description | Type | Default | Required | +| --- | --- | --- | --- | --- | +| `name` | Descriptive name for the source in Overmind | `string` | n/a | yes | +| `regions` | AWS regions to discover (defaults to all non-opt-in regions) | `list(string)` | All 17 standard regions | no | +| `role_name` | Name for the IAM role created in this account | `string` | `"overmind-read-only"` | no | +| `tags` | Additional tags to apply to IAM resources | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +| --- | --- | +| `role_arn` | ARN of the created IAM role | +| `source_id` | UUID of the Overmind source | +| `external_id` | AWS STS external ID used in the trust policy | + +## Importing Existing Sources + +If you already created an Overmind AWS source through the UI and want to bring it under Terraform management, you can import it using the source UUID. Find the UUID on the source details page in [Settings > Sources](https://app.overmind.tech/settings/sources). + +When using the module: + +```shell +terraform import module.overmind_aws_source.overmind_aws_source.this +``` + +When using the provider resource directly: + +```shell +terraform import overmind_aws_source.example +``` + +After importing, run `terraform plan` to verify the state matches your configuration. Terraform will show any drift between the imported resource and your HCL. + +Note that importing brings only the Overmind source under Terraform management. If the IAM role was also created outside of Terraform, you will need to import it separately with `terraform import aws_iam_role.overmind `. + +## Verify Your Source + +After `terraform apply` completes: + +1. Open [Settings > Sources](https://app.overmind.tech/settings/sources) in the Overmind app. +2. Your new source should appear with a green healthy status within about a minute. +3. Navigate to [Explore](https://app.overmind.tech/explore) to browse discovered resources. + +## Registry Links + +- **Terraform Registry**: [overmindtech/overmind provider](https://registry.terraform.io/providers/overmindtech/overmind/latest) | [overmindtech/aws-source module](https://registry.terraform.io/modules/overmindtech/aws-source/overmind/latest) +- **OpenTofu Registry**: coming soon + +## Troubleshooting + +### "Provider not found" during terraform init + +Ensure you are running Terraform >= 1.5.0 or OpenTofu >= 1.6.0, and that you have internet access to reach the registry. Run `terraform init -upgrade` to refresh provider caches. + +### "Unauthorized" or "invalid API key" + +Verify that `OVERMIND_API_KEY` is set and that the key has `sources:write` scope. You can check your API keys in [Settings > API Keys](https://app.overmind.tech/settings/api-keys). + +### "Access Denied" creating IAM resources + +The AWS credentials used by Terraform need permission to create IAM roles and policies. Verify your credentials have the `iam:CreateRole`, `iam:PutRolePolicy`, and `iam:CreatePolicy` permissions in the target account. + +### Source shows as unhealthy after apply + +The IAM role may take a few seconds to propagate. Wait one to two minutes and refresh the Sources page. If the source remains unhealthy, verify the role ARN in the AWS console matches the `role_arn` output. + +### Destroying resources + +`terraform destroy` cleanly removes both the IAM resources in AWS and the Overmind source registration. diff --git a/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md b/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md new file mode 100644 index 00000000..64c40288 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md @@ -0,0 +1,177 @@ +--- +title: Update IAM Role for Enhanced Security +sidebar_position: 2 +--- + +# Updating Your AWS IAM Role for Enhanced Security + +Starting December 2025, we've enhanced how Overmind connects to your AWS infrastructure using **EKS Pod Identity**. This update improves security by using short-lived, automatically rotated credentials when accessing your AWS resources. + +## Why This Update is Important + +Previously, Overmind used static AWS credentials to assume the IAM role in your account. With EKS Pod Identity, we now use: + +- **Short-lived credentials** that are automatically rotated +- **Session tagging** for better auditability and tracing +- **Reduced attack surface** with no long-lived credentials + +To benefit from these security improvements, you need to update the IAM role trust policy in your AWS account to allow the `sts:TagSession` permission. + +## How to Check if You Need to Update + +You can check if your IAM role needs updating by looking at the version tag: + +1. Open the **AWS IAM Console** +2. Navigate to **Roles** and find your Overmind role (usually named "Overmind" or "overmind-read-only") +3. Click on the role and go to the **Tags** tab +4. Look for the `overmind.version` tag + +| Version Tag | Status | +|-------------|--------| +| `2025-12-01` or later | ✅ Up to date | +| `2023-03-14` or earlier | ⚠️ Update required | +| No tag | ⚠️ Update required | + +## Update Instructions + +### Option A: Update via CloudFormation (Recommended) + +If you originally created your IAM role using our CloudFormation template, follow these steps: + +#### Step 1: Open AWS CloudFormation Console + +Go to the [AWS CloudFormation Console](https://console.aws.amazon.com/cloudformation) in the region where you deployed the Overmind stack. + +#### Step 2: Select the Overmind Stack + +Find and select the CloudFormation stack named **"Overmind"** (or "OvermindDevelopment" for development environments). + +:::tip +Look for a stack named "Overmind" or "OvermindDevelopment" in the region where you originally deployed it. +::: + +#### Step 3: Update the Stack + +1. Click the **"Update"** button at the top of the page +2. Under "Prepare template", select **"Replace existing template"** +3. Under "Specify template", select **"Amazon S3 URL"** +4. Enter the template URL provided by Overmind (see below for how to find it) +5. Click **"Next"** + +![Screenshot of AWS CloudFormation Update stack wizard showing "Replace existing template" selected and the Amazon S3 URL input field](./cloudformation-update-stack.png) + +:::info Finding the CloudFormation Template URL +To get the latest CloudFormation template URL: +1. Go to [Overmind Settings > Sources](https://app.overmind.tech/settings/sources) +2. Click **Add Source > AWS** +3. Right-click the "Deploy" button and copy the link - the URL contains the `templateURL` parameter +::: + +#### Step 4: Review and Apply + +1. Keep the existing **External ID** parameter unchanged +2. Click **"Next"** through the configuration pages +3. On the review page, check the box acknowledging that CloudFormation might create IAM resources +4. Click **"Submit"** + +The update typically takes less than a minute to complete. + +### Option B: Manual Update + +If you prefer to update the IAM role manually, or if you created the role without CloudFormation: + +#### Step 1: Open IAM Console + +Go to the [AWS IAM Console](https://console.aws.amazon.com/iam) and navigate to **Roles**. + +#### Step 2: Find Your Overmind Role + +Search for and select your Overmind role (usually named "Overmind" or the name you specified during setup). + +#### Step 3: Edit the Trust Policy + +1. Go to the **Trust relationships** tab +2. Click **"Edit trust policy"** +3. Add the following statement to the `Statement` array: + +```json +{ + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::944651592624:root" + }, + "Action": "sts:TagSession" +} +``` + +Your complete trust policy should look like this: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::944651592624:root" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "YOUR-EXTERNAL-ID-HERE" + } + } + }, + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::944651592624:root" + }, + "Action": "sts:TagSession" + } + ] +} +``` + +4. Click **"Update policy"** + +#### Step 4: Update the Version Tag (Optional) + +To help track the version of your role configuration: + +1. Go to the **Tags** tab +2. Add or update the tag: + - **Key:** `overmind.version` + - **Value:** `2025-12-01` + +## Verification + +After updating, your existing AWS sources will continue to work without interruption. The enhanced security features will be automatically enabled within the next few minutes. + +You can verify the update was successful by: +1. Checking that your source shows a green status in [Overmind Settings > Sources](https://app.overmind.tech/settings/sources) +2. Verifying the role's `overmind.version` tag shows `2025-12-01` or later + +## Frequently Asked Questions + +### Will this cause any downtime? + +No. The update adds a new permission without removing any existing permissions. Your sources will continue to work throughout the update process. + +### What if I have multiple AWS accounts? + +You'll need to update the IAM role in each AWS account where you have an Overmind source configured. + +### What happens if I don't update? + +Your sources will continue to work, but won't benefit from the enhanced security features provided by EKS Pod Identity. We strongly recommend updating for improved security posture. + +### I'm using a different Overmind AWS account ID + +If you're on a dedicated or on-premises deployment, the AWS account ID in the trust policy may be different. Contact your Overmind administrator for the correct account ID. + +## Need Help? + +If you encounter any issues during the update: + +- Contact support at support@overmind.tech diff --git a/docs.overmind.tech/docs/sources/embed.go b/docs.overmind.tech/docs/sources/embed.go new file mode 100644 index 00000000..e501df8a --- /dev/null +++ b/docs.overmind.tech/docs/sources/embed.go @@ -0,0 +1,11 @@ +// Package adapterdata embeds the per-type adapter metadata JSON files so +// other packages can look up category, descriptive name, supported query +// methods, and potential links without duplicating the data. +package adapterdata + +import "embed" + +// Files contains every adapter JSON file under {provider}/data/*.json. +// +//go:embed */data/*.json +var Files embed.FS diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-batch-prediction-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-batch-prediction-job.md new file mode 100644 index 00000000..58334249 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-batch-prediction-job.md @@ -0,0 +1,34 @@ +--- +title: GCP Ai Platform Batch Prediction Job +sidebar_label: gcp-ai-platform-batch-prediction-job +--- + +A GCP AI Platform (Vertex AI) Batch Prediction Job is a managed job that runs a trained model against a large, static dataset to generate predictions asynchronously. It allows you to score data stored in Cloud Storage or BigQuery and write the results back to either service, without having to manage your own compute infrastructure. For full details see the official documentation: https://docs.cloud.google.com/vertex-ai/docs/predictions/get-batch-predictions + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-batch-prediction-job by its "locations|batchPredictionJobs" +- ~~`LIST`~~ +- `SEARCH`: Search Batch Prediction Jobs within a location. Use the location name e.g., 'us-central1' + +## Possible Links + +### [`gcp-ai-platform-model`](/sources/gcp/Types/gcp-ai-platform-model) + +The batch prediction job references a trained model that provides the prediction logic. The job cannot run without specifying this model. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +Input data for a batch prediction can come from a BigQuery table, and the job can also write the prediction results to another BigQuery table. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +Customer-managed encryption keys (CMEK) from Cloud KMS may be attached to the job to encrypt its output artefacts stored in Cloud Storage or BigQuery. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +The job is executed under a specific IAM service account, which grants it permissions to read inputs, write outputs, and access the model. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Storage buckets are commonly used to supply the input files (in JSONL or CSV) and/or to store the prediction output files produced by the batch job. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-custom-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-custom-job.md new file mode 100644 index 00000000..4a4c1935 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-custom-job.md @@ -0,0 +1,35 @@ +--- +title: GCP Ai Platform Custom Job +sidebar_label: gcp-ai-platform-custom-job +--- + +A GCP AI Platform Custom Job (now part of Vertex AI) is a fully-managed training workload that runs user-supplied code inside one or more container images on Google Cloud infrastructure. It allows you to specify machine types, accelerators, networking and encryption settings, then orchestrates the provisioning, execution and clean-up of the training cluster. Custom Jobs are typically used when pre-built AutoML options are insufficient and you need complete control over your training loop. +Official documentation: https://cloud.google.com/vertex-ai/docs/training/create-custom-job + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-custom-job by its "name" +- `LIST`: List all gcp-ai-platform-custom-job +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-ai-platform-model`](/sources/gcp/Types/gcp-ai-platform-model) + +A successful Custom Job can optionally upload the trained artefacts as a Vertex AI Model resource; if that happens, the job will reference (and be referenced by) the resulting `gcp-ai-platform-model`. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +Custom Jobs support customer-managed encryption keys (CMEK). When a CMEK is specified, the job resource, its logs and any artefacts it creates are encrypted with the referenced `gcp-cloud-kms-crypto-key`. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +You can run Custom Jobs inside a specific VPC network to reach private data sources or to avoid egress to the public internet. In that case the job is linked to the chosen `gcp-compute-network`. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Execution of a Custom Job occurs under a user-specified service account, which determines the permissions the training containers possess. The job therefore has a direct relationship to a `gcp-iam-service-account`. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Training code commonly reads data from, and writes checkpoints or model artefacts to, Cloud Storage. The buckets used for staging, input or output will be surfaced as linked `gcp-storage-bucket` resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-endpoint.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-endpoint.md new file mode 100644 index 00000000..bcbe74fe --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-endpoint.md @@ -0,0 +1,35 @@ +--- +title: GCP Ai Platform Endpoint +sidebar_label: gcp-ai-platform-endpoint +--- + +A Vertex AI (formerly AI Platform) **Endpoint** is a regional resource that serves as an entry-point for online prediction requests in Google Cloud. One or more trained **Models** can be deployed to an Endpoint, after which client applications invoke the Endpoint’s HTTPS URL (or Private Service Connect address) to obtain real-time predictions. The resource stores configuration such as traffic splitting between models, logging settings, encryption settings and the VPC network to be used for private access. +Official documentation: https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.endpoints + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-endpoint by its "name" +- `LIST`: List all gcp-ai-platform-endpoint +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-ai-platform-model`](/sources/gcp/Types/gcp-ai-platform-model) + +An Endpoint may contain one or many `deployedModel` blocks, each of which references a separate Model resource. Overmind links the Endpoint to every Model that is currently deployed or that has traffic allocated to it. + +### [`gcp-ai-platform-model-deployment-monitoring-job`](/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job) + +If model-deployment monitoring has been enabled, the monitoring job resource records statistics and drift detection for a specific Endpoint. Overmind links the Endpoint to all monitoring jobs that target it. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +Prediction logging and monitoring can be configured to write request/response data into BigQuery tables. Those tables are therefore linked to the Endpoint that produced the records. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +Endpoints can be created with a Customer-Managed Encryption Key (CMEK) via the `encryptionSpec.kmsKeyName` field. Overmind links the Endpoint to the specific Cloud KMS CryptoKey it uses for at-rest encryption. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When an Endpoint is set up for private predictions, it must specify a VPC network (`network` field) that will be used for Private Service Connect. This creates a relationship between the Endpoint and the referenced Compute Network. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job.md new file mode 100644 index 00000000..69cb6b26 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job.md @@ -0,0 +1,32 @@ +--- +title: GCP Ai Platform Model Deployment Monitoring Job +sidebar_label: gcp-ai-platform-model-deployment-monitoring-job +--- + +A Model Deployment Monitoring Job in Vertex AI (formerly AI Platform) performs continuous evaluation of a model that has been deployed to an endpoint. The job collects prediction requests and responses, analyses them for data drift, feature skew, and other anomalies, and can raise alerts when thresholds are exceeded. This enables teams to detect issues in production models early and take corrective action before business impact occurs. + +Official documentation: https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.modelDeploymentMonitoringJobs + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-model-deployment-monitoring-job by its "locations|modelDeploymentMonitoringJobs" +- ~~`LIST`~~ +- `SEARCH`: Search Model Deployment Monitoring Jobs within a location. Use the location name e.g., 'us-central1' + +## Possible Links + +### [`gcp-ai-platform-endpoint`](/sources/gcp/Types/gcp-ai-platform-endpoint) + +A Model Deployment Monitoring Job is always attached to a specific Vertex AI endpoint; it monitors one or more model deployments that live on that endpoint. The link represents the `endpoint` field inside the job resource. + +### [`gcp-ai-platform-model`](/sources/gcp/Types/gcp-ai-platform-model) + +Within `modelDeploymentMonitoringObjectiveConfigs`, the job specifies the deployed model(s) it should watch. This link captures that relationship between the monitoring job and the underlying Vertex AI model resources. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the job is created with `encryptionSpec`, it uses a customer-managed Cloud KMS key to encrypt monitoring logs and metadata. The linked Crypto Key represents that key. + +### [`gcp-monitoring-notification-channel`](/sources/gcp/Types/gcp-monitoring-notification-channel) + +Alerting for drift or skew relies on Cloud Monitoring notification channels listed in the job’s `alertConfig.notificationChannels`. This link connects the monitoring job to those channels so users can trace how alerts will be delivered. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model.md new file mode 100644 index 00000000..6e685c19 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model.md @@ -0,0 +1,31 @@ +--- +title: GCP Ai Platform Model +sidebar_label: gcp-ai-platform-model +--- + +A **GCP AI Platform Model** (now part of Vertex AI) is a top-level resource that represents a machine-learning model and its metadata. It groups together one or more model versions (or “Model resources” in Vertex AI terminology), defines the serving container, encryption settings and access controls, and can be deployed to online prediction endpoints or used by batch prediction jobs. +For full details, see the official documentation: https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.models + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-model by its "name" +- `LIST`: List all gcp-ai-platform-model +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-ai-platform-endpoint`](/sources/gcp/Types/gcp-ai-platform-endpoint) + +An AI Platform Model can be deployed to one or more endpoints. When Overmind detects that a model has been deployed, it links the model to the corresponding `gcp-ai-platform-endpoint` resource so that you can see where the model is serving traffic. + +### [`gcp-ai-platform-pipeline-job`](/sources/gcp/Types/gcp-ai-platform-pipeline-job) + +Vertex AI Pipeline Jobs often produce models as artefacts at the end of a training pipeline. Overmind links a `gcp-ai-platform-pipeline-job` to the `gcp-ai-platform-model` it created (or updated) so you can trace the provenance of a model back to the pipeline run that generated it. + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +Models use a container image for prediction service. If that container image is stored in Artifact Registry, Overmind establishes a link between the model and the `gcp-artifact-registry-docker-image` representing the serving container. This highlights dependencies on specific container images and versions. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If Customer-Managed Encryption Keys (CMEK) are enabled for the model, the model resource references the Cloud KMS Crypto Key used to encrypt the model data at rest. Overmind links the model to the `gcp-cloud-kms-crypto-key` to surface encryption dependencies and potential key-rotation risks. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-pipeline-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-pipeline-job.md new file mode 100644 index 00000000..d15fdf9f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-pipeline-job.md @@ -0,0 +1,31 @@ +--- +title: GCP Ai Platform Pipeline Job +sidebar_label: gcp-ai-platform-pipeline-job +--- + +A **GCP AI Platform Pipeline Job** (now part of Vertex AI Pipelines) represents a managed execution of a Kubeflow pipeline on Google Cloud. It orchestrates a series of container-based tasks—such as data preprocessing, model training, and deployment—into a reproducible workflow that runs on Google-managed infrastructure. Each job stores its metadata, intermediate artefacts and logs in Google-hosted services, and can be monitored, retried or version-controlled through the Vertex AI console or API. +For full details, see the official documentation: [Vertex AI Pipelines – Run pipeline jobs](https://docs.cloud.google.com/vertex-ai/docs/pipelines/run-pipeline). + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-pipeline-job by its "name" +- `LIST`: List all gcp-ai-platform-pipeline-job +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A pipeline job can be configured to use customer-managed encryption keys (CMEK) so that all intermediate artefacts and metadata produced by the pipeline are encrypted with a specific Cloud KMS crypto key. Overmind therefore surfaces a link to the `gcp-cloud-kms-crypto-key` that protects the job’s resources. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Pipeline components often run on GKE clusters or custom training/serving services that are attached to a VPC network. When a job specifies a `network` or `privateClusterConfig`, Overmind links the job to the corresponding `gcp-compute-network`, highlighting network-level exposure or egress restrictions that may affect the pipeline. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every pipeline job executes under a service account whose IAM permissions determine which Google Cloud resources the job can access (e.g. storage buckets, BigQuery datasets). Overmind connects the job to that `gcp-iam-service-account` so that permission scopes and potential privilege escalations can be inspected. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Pipeline jobs read from and write to Cloud Storage for dataset ingestion, model artefact output and pipeline metadata storage. Any bucket referenced in the job’s `pipeline_root`, component arguments or logging configuration is linked here, allowing visibility into data residency, ACLs and lifecycle policies relevant to the pipeline’s operation. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-artifact-registry-docker-image.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-artifact-registry-docker-image.md new file mode 100644 index 00000000..ce46b462 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-artifact-registry-docker-image.md @@ -0,0 +1,17 @@ +--- +title: GCP Artifact Registry Docker Image +sidebar_label: gcp-artifact-registry-docker-image +--- + +A GCP Artifact Registry Docker Image resource represents a single immutable image stored in Google Cloud’s Artifact Registry. It contains metadata such as the image digest, tags, size and creation timestamp, and can be queried to understand exactly which layers and versions are about to be deployed. Managing this resource allows you to verify provenance, scan for vulnerabilities and enforce policies before the image ever reaches production. +For a full description of the REST resource, see Google’s official documentation: https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories.dockerImages + +**Terrafrom Mappings:** + +- `google_artifact_registry_docker_image.name` + +## Supported Methods + +- `GET`: Get a gcp-artifact-registry-docker-image by its "locations|repositories|dockerImages" +- ~~`LIST`~~ +- `SEARCH`: Search for Docker images in Artifact Registry. Use the format "location|repository_id" or "projects/[project]/locations/[location]/repository/[repository_id]/dockerImages/[docker_image]" which is supported for terraform mappings. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-data-transfer-transfer-config.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-data-transfer-transfer-config.md new file mode 100644 index 00000000..14a1cebc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-data-transfer-transfer-config.md @@ -0,0 +1,31 @@ +--- +title: GCP Big Query Data Transfer Transfer Config +sidebar_label: gcp-big-query-data-transfer-transfer-config +--- + +The BigQuery Data Transfer Service Transfer Config defines a scheduled data-transfer job in Google Cloud. It specifies where the data comes from (for example Google Ads, YouTube or an external Cloud Storage bucket), the destination BigQuery dataset, the refresh window, schedule, run-options, encryption settings and notification preferences. In essence, it is the canonical object that tells BigQuery Data Transfer Service what to move, when to move it and how to handle the resulting tables. +Official documentation: https://docs.cloud.google.com/bigquery/docs/working-with-transfers + +**Terrafrom Mappings:** + +- `google_bigquery_data_transfer_config.id` + +## Supported Methods + +- `GET`: Get a gcp-big-query-data-transfer-transfer-config by its "locations|transferConfigs" +- ~~`LIST`~~ +- `SEARCH`: Search for BigQuery Data Transfer transfer configs in a location. Use the format "location" or "projects/project_id/locations/location/transferConfigs/transfer_config_id" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +The transfer config’s `destinationDatasetId` points to the BigQuery dataset that will receive the imported data, so the config depends on – and is intrinsically linked to – that dataset. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If customer-managed encryption is enabled, the transfer config references a Cloud KMS CryptoKey that is used to encrypt the tables created by the transfer, creating a dependency on the key. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +Through the `notificationPubsubTopic` field, the transfer config can publish status and error messages about individual transfer runs to a Pub/Sub topic, establishing an outgoing link to that topic. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-dataset.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-dataset.md new file mode 100644 index 00000000..cccc66c4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-dataset.md @@ -0,0 +1,39 @@ +--- +title: GCP Big Query Dataset +sidebar_label: gcp-big-query-dataset +--- + +A BigQuery Dataset is a top-level container that holds BigQuery tables, views, models and routines, and defines the geographic location where that data is stored. It also acts as the unit for access control, default encryption configuration and data lifecycle policies. +For full details see the Google Cloud documentation: https://cloud.google.com/bigquery/docs/datasets + +**Terrafrom Mappings:** + +- `google_bigquery_dataset.dataset_id` + +## Supported Methods + +- `GET`: Get GCP Big Query Dataset by "gcp-big-query-dataset-id" +- `LIST`: List all GCP Big Query Dataset items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +A dataset can reference other datasets via authorised views or cross-dataset access entries. Those referenced datasets will be linked to the current item. + +### [`gcp-big-query-model`](/sources/gcp/Types/gcp-big-query-model) + +Every BigQuery ML model belongs to exactly one dataset. All models whose `dataset_id` matches this dataset will be linked. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +Tables and views are stored inside a dataset. All tables whose `dataset_id` equals this dataset will be linked. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the dataset is encrypted with a customer-managed key, the KMS Crypto Key used for default encryption will be linked here. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Service accounts that appear in the dataset’s IAM policy (for example as editors, owners, readers or custom roles) will be linked to show who can access or manage the dataset. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-model.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-model.md new file mode 100644 index 00000000..d570b152 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-model.md @@ -0,0 +1,26 @@ +--- +title: GCP Big Query Model +sidebar_label: gcp-big-query-model +--- + +A BigQuery Model is a logical resource that stores the metadata and artefacts produced by BigQuery ML when you train a machine-learning model. It lives inside a BigQuery dataset and can subsequently be queried, evaluated, exported or further trained. For a full description see the official Google Cloud documentation: https://cloud.google.com/bigquery/docs/reference/rest/v2/models + +## Supported Methods + +- `GET`: Get GCP Big Query Model by "gcp-big-query-dataset-id|gcp-big-query-model-id" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Big Query Model by "gcp-big-query-model-id" + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +Each model is contained within exactly one BigQuery dataset. The link represents this parent–child relationship and allows Overmind to surface the impact of changes to the dataset on the model. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +A model is usually trained from, and may reference, one or more BigQuery tables (for example, the training, validation and prediction input tables). This link lets Overmind trace how alterations to those tables could affect the model’s behaviour or validity. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If customer-managed encryption keys (CMEK) are enabled, the model’s data is encrypted with a Cloud KMS crypto-key. Linking the model to the crypto-key allows Overmind to assess the consequences of key rotation, deletion or permission changes on the model’s availability. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-routine.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-routine.md new file mode 100644 index 00000000..b3534ec1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-routine.md @@ -0,0 +1,22 @@ +--- +title: GCP Big Query Routine +sidebar_label: gcp-big-query-routine +--- + +A BigQuery Routine represents a user-defined piece of reusable logic—such as a stored procedure or user-defined function—that is stored inside a BigQuery dataset and can be invoked from SQL. Routines let teams encapsulate data-processing logic, share it across queries, and manage it with version control and Infrastructure-as-Code tools. For a full description of the capabilities and configuration options, see the Google Cloud documentation on routines (https://cloud.google.com/bigquery/docs/routines-intro). + +**Terrafrom Mappings:** + +- `google_bigquery_routine.routine_id` + +## Supported Methods + +- `GET`: Get GCP Big Query Routine by "gcp-big-query-dataset-id|gcp-big-query-routine-id" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Big Query Routine by "gcp-big-query-routine-id" + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +A routine is defined within a specific BigQuery dataset; the link shows the parent dataset that contains the routine. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-table.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-table.md new file mode 100644 index 00000000..77f8bde5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-table.md @@ -0,0 +1,26 @@ +--- +title: GCP Big Query Table +sidebar_label: gcp-big-query-table +--- + +A BigQuery table is the fundamental unit of storage in Google Cloud BigQuery. It holds the rows of structured data that analysts query using SQL, and it defines the schema, partitioning, clustering, and encryption settings that govern how that data is stored and accessed. For a full description see the Google Cloud documentation: https://cloud.google.com/bigquery/docs/tables + +**Terrafrom Mappings:** + +- `google_bigquery_table.id` + +## Supported Methods + +- `GET`: Get GCP Big Query Table by "gcp-big-query-dataset-id|gcp-big-query-table-id" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Big Query Table by "gcp-big-query-dataset-id" + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +Every BigQuery table is contained within exactly one dataset. This link represents that parent–child relationship, enabling Overmind to trace from a table back to the dataset that organises and administers it. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If a BigQuery table is encrypted with a customer-managed encryption key (CMEK), this link points to the specific Cloud KMS crypto key in use. It allows Overmind to surface risks associated with key rotation, permissions, or key deletion that could affect the table’s availability or compliance posture. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-app-profile.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-app-profile.md new file mode 100644 index 00000000..76c988b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-app-profile.md @@ -0,0 +1,27 @@ +--- +title: GCP Big Table Admin App Profile +sidebar_label: gcp-big-table-admin-app-profile +--- + +A Bigtable **App Profile** is a logical wrapper that tells Cloud Bigtable _how_ an application’s traffic should be routed, which clusters it can use, and what fail-over behaviour to apply. By creating multiple app profiles you can isolate workloads, direct different applications to specific clusters, or enable multi-cluster routing for higher availability. +For an in-depth explanation see the official documentation: https://cloud.google.com/bigtable/docs/app-profiles + +**Terrafrom Mappings:** + +- `google_bigtable_app_profile.id` + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-app-profile by its "instances|appProfiles" +- ~~`LIST`~~ +- `SEARCH`: Search for BigTable App Profiles in an instance. Use the format "instance" or "projects/[project_id]/instances/[instance_name]/appProfiles/[app_profile_id]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-big-table-admin-cluster`](/sources/gcp/Types/gcp-big-table-admin-cluster) + +Every app profile specifies one or more clusters that client traffic may reach. Therefore an App Profile is directly linked to the Bigtable Cluster(s) it can route requests to. + +### [`gcp-big-table-admin-instance`](/sources/gcp/Types/gcp-big-table-admin-instance) + +An App Profile always belongs to exactly one Bigtable Instance; it cannot exist outside that instance’s administrative scope. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-backup.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-backup.md new file mode 100644 index 00000000..58520f6c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-backup.md @@ -0,0 +1,27 @@ +--- +title: GCP Big Table Admin Backup +sidebar_label: gcp-big-table-admin-backup +--- + +A Cloud Bigtable Backup is a point-in-time copy of a Bigtable table that is managed by the Bigtable Admin API. It allows you to protect data against accidental deletion or corruption and to restore the table later, either in the same cluster or in a different one within the same instance. Each backup is stored in a specific cluster, retains the table’s schema and data as they existed at the moment the backup was taken, and can be kept for a user-defined retention period. +Official documentation: https://docs.cloud.google.com/bigtable/docs/backups + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-backup by its "instances|clusters|backups" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-big-table-admin-backup by its "instances|clusters" + +## Possible Links + +### [`gcp-big-table-admin-backup`](/sources/gcp/Types/gcp-big-table-admin-backup) + +The current item represents the Backup resource itself, containing metadata such as name, creation time, size, expiration time and the source table it protects. + +### [`gcp-big-table-admin-cluster`](/sources/gcp/Types/gcp-big-table-admin-cluster) + +Each backup is physically stored in exactly one Bigtable cluster; this link shows the parent cluster that owns and stores the backup. + +### [`gcp-big-table-admin-table`](/sources/gcp/Types/gcp-big-table-admin-table) + +A backup is created from a specific table; this link identifies that source table and allows you to see which tables can be restored from the backup. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-cluster.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-cluster.md new file mode 100644 index 00000000..c0a71a23 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-cluster.md @@ -0,0 +1,23 @@ +--- +title: GCP Big Table Admin Cluster +sidebar_label: gcp-big-table-admin-cluster +--- + +A Cloud Bigtable cluster represents the set of serving and storage resources that handle all reads and writes for a Cloud Bigtable instance. Each cluster belongs to a single instance, lives in one Google Cloud zone, and is configured with a certain number of nodes and a specific storage type (SSD or HDD). Clusters can be added or removed to provide high availability, geographic redundancy, or additional throughput. With Overmind you can surface mis-configurations such as a single-zone deployment, inadequate node counts, or missing encryption settings before your change reaches production. +Official Google documentation: https://cloud.google.com/bigtable/docs/overview#clusters + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-cluster by its "instances|clusters" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-big-table-admin-cluster by its "instances" + +## Possible Links + +### [`gcp-big-table-admin-instance`](/sources/gcp/Types/gcp-big-table-admin-instance) + +Every cluster is a child resource of a Cloud Bigtable instance. Overmind links the cluster back to its parent instance so you can see which database workloads will be affected if you modify or delete the cluster. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +When customer-managed encryption keys (CMEK) are enabled for a Bigtable cluster, the cluster references a Cloud KMS crypto key. Overmind creates a link to that key so you can verify the key’s status, rotation schedule, and IAM policy before deploying changes to the cluster. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-instance.md new file mode 100644 index 00000000..7c8d7a88 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-instance.md @@ -0,0 +1,24 @@ +--- +title: GCP Big Table Admin Instance +sidebar_label: gcp-big-table-admin-instance +--- + +Google Cloud Bigtable is Google’s fully managed, scalable NoSQL database service. +A Bigtable _instance_ is the administrative parent resource that defines the geographic placement, replication strategy, encryption settings and service-level configuration for the tables that will live inside it. Every instance contains one or more clusters, and each cluster in turn contains the nodes that serve user data. Creating or modifying an instance therefore determines where and how your Bigtable data will be stored and replicated. +For further details, refer to the official Google Cloud documentation: https://cloud.google.com/bigtable/docs/instances-clusters-nodes + +**Terrafrom Mappings:** + +- `google_bigtable_instance.name` + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-instance by its "name" +- `LIST`: List all gcp-big-table-admin-instance +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-big-table-admin-cluster`](/sources/gcp/Types/gcp-big-table-admin-cluster) + +A Bigtable Admin Instance is the parent of one or more Bigtable Admin Clusters. Each cluster resource belongs to exactly one instance, inheriting its replication and localisation settings. When Overmind discovers or updates a gcp-big-table-admin-instance, it follows this relationship to enumerate the gcp-big-table-admin-cluster resources that compose the instance’s underlying serving infrastructure. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-table.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-table.md new file mode 100644 index 00000000..a2196b32 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-table.md @@ -0,0 +1,30 @@ +--- +title: GCP Big Table Admin Table +sidebar_label: gcp-big-table-admin-table +--- + +Google Cloud Bigtable tables are the primary data containers inside a Bigtable instance. A table holds rows of schemaless, wide-column data that can scale to petabytes while maintaining low-latency access. The Admin Table resource represents the configuration and lifecycle metadata for a table (for example, column families, garbage-collection rules, encryption settings and replication state). For a detailed explanation see the official documentation: https://docs.cloud.google.com/bigtable/docs/reference/admin/rpc. + +**Terrafrom Mappings:** + +- `google_bigtable_table.id` + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-table by its "instances|tables" +- ~~`LIST`~~ +- `SEARCH`: Search for BigTable tables in an instance. Use the format "instance_name" or "projects/[project_id]/instances/[instance_name]/tables/[table_name]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-big-table-admin-backup`](/sources/gcp/Types/gcp-big-table-admin-backup) + +A backup is a point-in-time snapshot that is created from a specific table. From a table resource you can enumerate the backups that protect it, or follow a backup back to the source table from which it was taken. + +### [`gcp-big-table-admin-instance`](/sources/gcp/Types/gcp-big-table-admin-instance) + +Every table belongs to exactly one Bigtable instance. The instance is the parent container that defines the clusters, replication topology and IAM policy under which the table operates. + +### [`gcp-big-table-admin-table`](/sources/gcp/Types/gcp-big-table-admin-table) + +Tables of the same type within the same project or instance can be cross-referenced for comparison, migration or restore operations (for example, when restoring a backup into a new table). Overmind links tables to other tables so you can trace relationships such as clone targets, restore destinations or sibling tables in the same instance. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-billing-billing-info.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-billing-billing-info.md new file mode 100644 index 00000000..59bf138c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-billing-billing-info.md @@ -0,0 +1,20 @@ +--- +title: GCP Cloud Billing Billing Info +sidebar_label: gcp-cloud-billing-billing-info +--- + +`gcp-cloud-billing-billing-info` represents a Google Cloud **ProjectBillingInfo** resource, i.e. the object that records which Cloud Billing Account a particular GCP project is attached to and whether billing is currently enabled. +Knowing which Billing Account is used – and whether charges can actually accrue – is often vital when assessing the financial risk of a new deployment. +Official documentation: https://cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo + +## Supported Methods + +- `GET`: Get a gcp-cloud-billing-billing-info by its "name" +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +Every ProjectBillingInfo belongs to exactly one Cloud project. Overmind therefore links the `gcp-cloud-billing-billing-info` item to the corresponding `gcp-cloud-resource-manager-project` item, allowing you to trace billing-account associations back to the project that will generate the spend. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-build-build.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-build-build.md new file mode 100644 index 00000000..0ecba466 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-build-build.md @@ -0,0 +1,31 @@ +--- +title: GCP Cloud Build Build +sidebar_label: gcp-cloud-build-build +--- + +A GCP Cloud Build Build represents a single execution of Google Cloud Build, Google’s fully-managed continuous integration and delivery service. A build encapsulates the series of build steps, source code location, build artefacts, substitutions and metadata that are executed within an isolated builder environment. Each build is uniquely identified by its `name` (formatted as `projects/{projectId}/builds/{buildId}`) and records status, timing information, logs location and any images or other artefacts produced. +For full details see the official documentation: https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds + +## Supported Methods + +- `GET`: Get a gcp-cloud-build-build by its "name" +- `LIST`: List all gcp-cloud-build-build +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +If the build definition contains a step that builds and pushes a Docker image, the resulting image is usually pushed to Artifact Registry. The build therefore produces — and is linked to — one or more `gcp-artifact-registry-docker-image` resources representing the images it published. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Cloud Build execution runs under a specific IAM service account (commonly the project-level Cloud Build service account or a custom account) which grants it permissions to fetch source, write logs and push artefacts. The build is thus associated with the `gcp-iam-service-account` used during its execution. + +### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) + +Cloud Build streams build logs to Cloud Logging; organisations often route these logs into dedicated Logging buckets for retention or analysis. When such routing is configured, the build’s log entries will appear in (and therefore relate to) the relevant `gcp-logging-bucket`. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Source code for a build can be fetched from a Cloud Storage bucket, and build logs or artefact archives can also be stored in buckets created by Cloud Build (e.g. `gs://{projectId}_cloudbuild`). Consequently, a build may read from or write to one or more `gcp-storage-bucket` resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-functions-function.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-functions-function.md new file mode 100644 index 00000000..119b1da2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-functions-function.md @@ -0,0 +1,34 @@ +--- +title: GCP Cloud Functions Function +sidebar_label: gcp-cloud-functions-function +--- + +A Google Cloud Functions Function is a serverless, event-driven compute resource that executes user-supplied code in response to HTTP requests or a wide range of Google Cloud events. Because Google Cloud manages the underlying infrastructure, you only specify the code, runtime, memory, timeout, trigger and IAM policy, and you are billed solely for the resources actually consumed while the function is running. For more detail, see Google’s official documentation: https://cloud.google.com/functions/docs/concepts/overview. + +## Supported Methods + +- `GET`: Get a gcp-cloud-functions-function by its "locations|functions" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-cloud-functions-function by its "locations" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If Customer-Managed Encryption Keys (CMEK) are enabled, the function’s source code, environment variables or secret volumes are encrypted with a Cloud KMS CryptoKey. Overmind links the function to any CryptoKey that protects its assets so you can assess key rotation or deletion risks. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Cloud Function runs as an IAM Service Account. The permissions granted to this account define what the function can read or modify at runtime. Overmind links the function to its execution service account, allowing you to evaluate privilege levels and potential lateral-movement paths. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +A function can be triggered by a Pub/Sub topic or publish messages to one. Overmind records these relationships so you can see which topics will invoke the function and what downstream systems might be affected if the function misbehaves. + +### [`gcp-run-service`](/sources/gcp/Types/gcp-run-service) + +Second-generation Cloud Functions are deployed on Cloud Run. Overmind links the function to the underlying Cloud Run Service, exposing additional configuration such as VPC connectors, ingress settings and revision history that may introduce risk. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Functions often interact with Cloud Storage: source code may be stored in a staging bucket, and functions can be triggered by bucket events (e.g., object creation). Overmind links the function to any associated buckets, helping you identify data-exfiltration risks and unintended public access. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key-version.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key-version.md new file mode 100644 index 00000000..64bb5960 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key-version.md @@ -0,0 +1,30 @@ +--- +title: GCP Cloud KMS Crypto Key Version +sidebar_label: gcp-cloud-kms-crypto-key-version +--- + +A CryptoKeyVersion represents an individual cryptographic key and its associated key material within a Cloud KMS CryptoKey. An ENABLED version can be used for cryptographic operations. Each CryptoKey can have multiple versions, allowing for key rotation. For security reasons, the raw cryptographic key material can never be viewed or exported - it can only be used to encrypt, decrypt, or sign data when an authorized user or application invokes Cloud KMS. For more information, refer to the [official documentation](https://docs.cloud.google.com/kms/docs/key-states). + +**Terraform Mappings:** + +- `google_kms_crypto_key_version.id` + +## Supported Methods + +- `GET`: Get GCP Cloud KMS Crypto Key Version by "location|keyRing|cryptoKey|version" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Cloud KMS Crypto Key Versions by "location|keyRing|cryptoKey" (returns all versions of the specified CryptoKey) + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A CryptoKeyVersion belongs to exactly one parent CryptoKey. The parent CryptoKey contains the version's configuration and purpose. Deleting the parent CryptoKey will delete all of its CryptoKeyVersions, but deleting a CryptoKeyVersion does not affect the parent key. + +### `gcp-cloudkms-importjob` + +If the key material was imported (rather than generated by KMS), the CryptoKeyVersion references the ImportJob that was used for the import operation. The ImportJob contains metadata about how the key material was imported. Deleting the ImportJob after a successful import does not affect the CryptoKeyVersion. + +### `gcp-cloudkms-ekmconnection` + +For CryptoKeyVersions with EXTERNAL_VPC protection level, the version links to an EKM (External Key Manager) connection that manages the external key material. This is used when keys are stored and operated on in an external key management system rather than within Google Cloud KMS. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key.md new file mode 100644 index 00000000..2813c0fb --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key.md @@ -0,0 +1,19 @@ +--- +title: GCP Cloud Kms Crypto Key +sidebar_label: gcp-cloud-kms-crypto-key +--- + +A Google Cloud KMS Crypto Key is a logical key resource that performs cryptographic operations such as encryption/de-encryption, signing, and message authentication. Each Crypto Key sits inside a Key Ring, which in turn lives in a specific GCP location (region). The key material for a Crypto Key can be rotated, versioned, and protected by Cloud KMS or by customer-managed hardware security modules, and it is referenced by other Google Cloud services whenever those services need to encrypt or sign data on your behalf. +Official documentation: https://cloud.google.com/kms/docs/object-hierarchy#key + +## Supported Methods + +- `GET`: Get GCP Cloud Kms Crypto Key by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Cloud Kms Crypto Key by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name" + +## Possible Links + +### [`gcp-cloud-kms-key-ring`](/sources/gcp/Types/gcp-cloud-kms-key-ring) + +A Crypto Key is always a child resource of a Key Ring. The `gcp-cloud-kms-key-ring` link allows Overmind to trace from the key to its parent container, establishing the hierarchical relationship needed to understand inheritance of IAM policies, location constraints, and aggregated risk. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-key-ring.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-key-ring.md new file mode 100644 index 00000000..5dd85555 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-key-ring.md @@ -0,0 +1,22 @@ +--- +title: GCP Cloud Kms Key Ring +sidebar_label: gcp-cloud-kms-key-ring +--- + +A Cloud KMS Key Ring is a logical container used to group related customer-managed encryption keys within Google Cloud’s Key Management Service (KMS). All Crypto Keys created inside the same Key Ring share the same geographic location, and access control can be applied at the Key Ring level to govern every key it contains. For more information, refer to the [official documentation](https://cloud.google.com/kms/docs/create-key-ring). + +**Terrafrom Mappings:** + +- `google_kms_key_ring.name` + +## Supported Methods + +- `GET`: Get GCP Cloud Kms Key Ring by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name" +- `LIST`: List all GCP Cloud Kms Key Rings across all locations in the project +- `SEARCH`: Search for GCP Cloud Kms Key Ring by "gcp-cloud-kms-key-ring-location" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Key Ring is the direct parent of one or more Crypto Keys. Every Crypto Key resource must belong to exactly one Key Ring, so Overmind creates this link to allow navigation from the Key Ring to all the keys it contains (and vice-versa), making it easier to assess the full cryptographic surface associated with a given deployment. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-project.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-project.md new file mode 100644 index 00000000..19130df9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-project.md @@ -0,0 +1,20 @@ +--- +title: GCP Cloud Resource Manager Project +sidebar_label: gcp-cloud-resource-manager-project +--- + +A **Google Cloud Platform (GCP) Project** is the fundamental organising entity managed by the Cloud Resource Manager service. Every GCP workload—whether it is a single virtual machine or a complex, multi-region Kubernetes deployment—must reside inside a Project. The Project acts as a logical container for: + +- All GCP resources (compute, storage, networking, databases, etc.) +- Identity and Access Management (IAM) policies +- Billing configuration +- Quotas and limits +- Metadata such as labels and organisation/folder hierarchy + +Because policies and billing are enforced at the Project level, understanding the state of a Project is critical when assessing deployment risk. For detailed information, refer to the official Google documentation: https://cloud.google.com/resource-manager/docs/creating-managing-projects + +## Supported Methods + +- `GET`: Get a gcp-cloud-resource-manager-project by its "name" +- ~~`LIST`~~ +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-tag-value.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-tag-value.md new file mode 100644 index 00000000..bcf456a7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-tag-value.md @@ -0,0 +1,16 @@ +--- +title: GCP Cloud Resource Manager Tag Value +sidebar_label: gcp-cloud-resource-manager-tag-value +--- + +A Tag Value is the value component of Google Cloud’s hierarchical tagging system, which allows you to attach fine-grained, policy-aware metadata to resources. Each Tag Value sits under a Tag Key and, together, the pair forms a tag that can be propagated across projects and folders within an organisation. Tags enable centralised governance, cost allocation, and conditional access control through IAM and Org Policy. For full details, see the official Google Cloud documentation: https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing#tag-values + +**Terrafrom Mappings:** + +- `google_tags_tag_value.name` + +## Supported Methods + +- `GET`: Get a gcp-cloud-resource-manager-tag-value by its "name" +- ~~`LIST`~~ +- `SEARCH`: Search for TagValues by TagKey. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md new file mode 100644 index 00000000..b9b328cd --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Address +sidebar_label: gcp-compute-address +--- + +A GCP Compute Address is a statically-reserved IPv4 or IPv6 address that can be assigned to Compute Engine resources such as virtual machine instances, forwarding rules, VPN gateways and load-balancers. Reserving the address stops it from changing when the attached resource is restarted and allows the address to be re-used on other resources later. Addresses may be global (for external HTTP(S) load-balancers) or regional (for most other use-cases), and internal addresses can be tied to a specific VPC network and sub-network. +For full details see the official documentation: https://docs.cloud.google.com/compute/docs/reference/rest/v1/addresses + +**Terrafrom Mappings:** + +- `google_compute_address.name` + +## Supported Methods + +- `GET`: Get GCP Compute Address by "gcp-compute-address-name" +- `LIST`: List all GCP Compute Address items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-address`](/sources/gcp/Types/gcp-compute-address) + +A self-link that allows Overmind to relate this address to other instances of the same type (for example, distinguishing between regional and global addresses with identical names). + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Internal (private) addresses are reserved within a specific VPC network, so an address will be linked to the `gcp-compute-network` that owns the IP range from which it is allocated. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +When an internal address is scoped to a particular sub-network, Overmind records this dependency by linking the address to the corresponding `gcp-compute-subnetwork`. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md new file mode 100644 index 00000000..7d468e6d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Autoscaler +sidebar_label: gcp-compute-autoscaler +--- + +The Google Cloud Compute Autoscaler is a regional or zonal resource that automatically adds or removes VM instances from a Managed Instance Group in response to workload demand. By scaling on CPU utilisation, load-balancing capacity, Cloud Monitoring metrics, or pre-defined schedules, it helps keep applications responsive while keeping infrastructure spending under control. +For detailed information, consult the official documentation: https://cloud.google.com/compute/docs/autoscaler + +**Terrafrom Mappings:** + +- `google_compute_autoscaler.name` + +## Supported Methods + +- `GET`: Get GCP Compute Autoscaler by "gcp-compute-autoscaler-name" +- `LIST`: List all GCP Compute Autoscaler items +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-backend-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-backend-service.md new file mode 100644 index 00000000..f295c81b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-backend-service.md @@ -0,0 +1,29 @@ +--- +title: GCP Compute Backend Service +sidebar_label: gcp-compute-backend-service +--- + +A GCP Compute Backend Service is the central configuration object that tells a Google Cloud load balancer where and how to send traffic. +It groups one or more back-end targets (for example instance groups, zonal NEG or serverless NEG), specifies the load-balancing scheme (internal or external), session affinity, health checks, protocol, timeout and (optionally) Cloud Armor security policies. +Because almost every Google Cloud load-balancing product routes traffic through a backend service, it is a critical part of any production deployment. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/backendServices + +**Terrafrom Mappings:** + +- `google_compute_backend_service.name` + +## Supported Methods + +- `GET`: Get GCP Compute Backend Service by "gcp-compute-backend-service-name" +- `LIST`: List all GCP Compute Backend Service items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +A backend service implicitly belongs to the same VPC network as the back-end resources (instance groups or NEGs) it references. Consequently, the service’s reachability, IP ranges and firewall posture are constrained by that network, so Overmind creates a link to the corresponding `gcp-compute-network` to surface these dependencies. + +### [`gcp-compute-security-policy`](/sources/gcp/Types/gcp-compute-security-policy) + +If Cloud Armor is enabled, the backend service contains a direct reference to a `securityPolicy`. This link allows Overmind to show how web-application-firewall rules and rate-limiting policies are applied to traffic flowing through the backend service. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md new file mode 100644 index 00000000..6b34dc83 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md @@ -0,0 +1,39 @@ +--- +title: GCP Compute Disk +sidebar_label: gcp-compute-disk +--- + +A GCP Compute Disk is a durable, high-performance block-storage volume that can be attached to one or more Compute Engine virtual machine instances. Persistent disks can act as boot devices or as additional data volumes, are automatically replicated within a zone or region, and can be backed up through snapshots or turned into custom images for rapid redeployment. +For full details see the official Google Cloud documentation: https://cloud.google.com/compute/docs/disks + +**Terrafrom Mappings:** + +- `google_compute_disk.name` + +## Supported Methods + +- `GET`: Get GCP Compute Disk by "gcp-compute-disk-name" +- `LIST`: List all GCP Compute Disk items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +This link appears when one persistent disk has been cloned or recreated from another (for example, using the `--source-disk` flag), allowing Overmind to follow ancestry or duplication chains between disks. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +A custom image may have been created from the current disk, or conversely the disk may have been created from an image. Overmind records this link so you can see which images depend on, or are the origin of, a particular disk. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +Virtual machine instances to which the disk is attached (either as a boot disk or as an additional mounted volume) are linked here. This allows you to view the blast-radius of any change to the disk in terms of running workloads. + +### [`gcp-compute-instant-snapshot`](/sources/gcp/Types/gcp-compute-instant-snapshot) + +If an instant snapshot has been taken from the disk, or if the disk has been created from an instant snapshot, Overmind records the relationship via this link. + +### [`gcp-compute-snapshot`](/sources/gcp/Types/gcp-compute-snapshot) + +Standard persistent disk snapshots derived from the disk, or snapshots that were used to create the disk, are linked here, enabling traceability between long-term backups and the live volume. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-external-vpn-gateway.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-external-vpn-gateway.md new file mode 100644 index 00000000..5877253d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-external-vpn-gateway.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute External Vpn Gateway +sidebar_label: gcp-compute-external-vpn-gateway +--- + +A GCP Compute External VPN Gateway represents a VPN gateway device that resides outside of Google Cloud—typically an on-premises firewall, router or a third-party cloud appliance. In High-Availability VPN (HA VPN) configurations it is used to describe the peer gateway so that Cloud Router and HA VPN tunnels can be created and managed declaratively. Each external gateway resource records the device’s public IP addresses and routing style, allowing Google Cloud to treat the remote endpoint as a first-class object and to validate or reference it from other VPN and network resources. +For full details, see the official Google documentation: https://cloud.google.com/sdk/gcloud/reference/compute/external-vpn-gateways + +**Terrafrom Mappings:** + +- `google_compute_external_vpn_gateway.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-external-vpn-gateway by its "name" +- `LIST`: List all gcp-compute-external-vpn-gateway +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md new file mode 100644 index 00000000..074bee1a --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md @@ -0,0 +1,26 @@ +--- +title: GCP Compute Firewall +sidebar_label: gcp-compute-firewall +--- + +A GCP Compute Firewall is a set of rules that control incoming and outgoing network traffic to Virtual Machine (VM) instances within a Google Cloud Virtual Private Cloud (VPC) network. Each rule defines whether specific connections (identified by protocol, port, source, destination and direction) are allowed or denied, thereby providing network-level security and segmentation for workloads running on Google Cloud. + +**Terrafrom Mappings:** + +- `google_compute_firewall.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-firewall by its "name" +- `LIST`: List all gcp-compute-firewall +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +A firewall rule is always created inside a single VPC network; that network determines the scope within which the rule is evaluated. Overmind therefore links a gcp-compute-firewall to the gcp-compute-network that owns it. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Firewall rules can specify target or source service accounts, allowing traffic to be filtered based on the workload identity running on a VM. Overmind links the firewall rule to any gcp-iam-service-account referenced in its `target_service_accounts` or `source_service_accounts` fields. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-forwarding-rule.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-forwarding-rule.md new file mode 100644 index 00000000..9d622a78 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-forwarding-rule.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Forwarding Rule +sidebar_label: gcp-compute-forwarding-rule +--- + +A GCP Compute Forwarding Rule defines how incoming packets are directed within Google Cloud. It associates an IP address, protocol and port range with a specific target—such as a load-balancer target proxy, VPN gateway, or, for certain internal load-balancer variants, a backend service—so that traffic is forwarded correctly. Forwarding rules can be global or regional and, when internal, are bound to a particular VPC network (and optionally a subnetwork) to control the scope of traffic distribution. +For full details see the official documentation: https://docs.cloud.google.com/load-balancing/docs/forwarding-rule-concepts + +**Terrafrom Mappings:** + +- `google_compute_forwarding_rule.name` + +## Supported Methods + +- `GET`: Get GCP Compute Forwarding Rule by "gcp-compute-forwarding-rule-name" +- `LIST`: List all GCP Compute Forwarding Rule items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-backend-service`](/sources/gcp/Types/gcp-compute-backend-service) + +For certain internal load balancers (e.g. Internal TCP/UDP Load Balancer), the forwarding rule points directly to a backend service. Overmind records this as a link so that any risk identified on the backend service can be surfaced when assessing the forwarding rule. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +An internal forwarding rule is created inside a specific VPC network; the rule determines how traffic is routed within that network. Linking the forwarding rule to its VPC allows Overmind to trace network-level misconfigurations that could affect traffic flow. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +When a regional internal forwarding rule is restricted to a particular subnetwork, the subnetwork is explicitly referenced. This link lets Overmind evaluate subnet-level controls (such as secondary ranges and IAM bindings) in the context of the forwarding rule’s traffic path. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-address.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-address.md new file mode 100644 index 00000000..85788cc6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-address.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Global Address +sidebar_label: gcp-compute-global-address +--- + +A Compute Global Address is a static, reserved IP address that is accessible from any Google Cloud region. It can be either external (public) or internal, and is typically used by globally distributed resources such as HTTP(S) load balancers, Cloud Run services, or global internal load balancers. Once reserved, the address can be bound to forwarding rules or other network endpoints, ensuring that the same IP is advertised worldwide. +For full details, see the official documentation: https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#global_addresses + +**Terrafrom Mappings:** + +- `google_compute_global_address.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-global-address by its "name" +- `LIST`: List all gcp-compute-global-address +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Global internal addresses must be created within a specific VPC network, and the `network` attribute on the address points to that VPC. Overmind therefore links a gcp-compute-global-address to the corresponding gcp-compute-network so that you can understand which network context the IP address belongs to and assess any related risks. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-forwarding-rule.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-forwarding-rule.md new file mode 100644 index 00000000..fae8d4a0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-forwarding-rule.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Global Forwarding Rule +sidebar_label: gcp-compute-global-forwarding-rule +--- + +A Google Compute Engine **Global Forwarding Rule** represents the externally-visible IP address and port(s) that receive traffic for a global load balancer. It defines where packets that enter on a particular protocol/port combination should be sent, pointing them at a target proxy (for HTTP(S), SSL or TCP Proxy load balancers) or target VPN gateway. In the case of Internal Global Load Balancing it may also specify the VPC network and subnetwork that own the virtual IP address. In short, the forwarding rule is the public (or internal) entry-point that maps client traffic to the load balancer’s control plane. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/globalForwardingRules + +**Terrafrom Mappings:** + +- `google_compute_global_forwarding_rule.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-global-forwarding-rule by its "name" +- `LIST`: List all gcp-compute-global-forwarding-rule +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-backend-service`](/sources/gcp/Types/gcp-compute-backend-service) + +A global forwarding rule ultimately delivers traffic to one or more backend services via a chain of resources (target proxy → URL map → backend service). Overmind surfaces this indirect relationship so that you can trace the path from the exposed IP address all the way to the workloads that will handle the request. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When the forwarding rule is used for internal global load balancing, it contains a `network` field that points to the VPC network that owns the virtual IP address. This link allows Overmind to show which network the listener lives in and what other resources share that network. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Similar to the network link, internal forwarding rules may reference a specific `subnetwork`. Overmind records this connection so you can identify the exact IP range and region in which the internal load balancer’s virtual IP is allocated. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-health-check.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-health-check.md new file mode 100644 index 00000000..61efaaff --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-health-check.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Health Check +sidebar_label: gcp-compute-health-check +--- + +A **GCP Compute Health Check** is a monitored probe that periodically tests the reachability and responsiveness of Google Cloud resources—such as VM instances, managed instance groups, or back-ends behind a load balancer—and reports their health status. These checks allow Google Cloud’s load balancers and auto-healing mechanisms to route traffic only to healthy instances, improving service reliability and availability. You can configure different protocols (HTTP, HTTPS, TCP, SSL, or HTTP/2), thresholds, and time-outs to suit your workload’s requirements. +For full details, see the official documentation: https://cloud.google.com/load-balancing/docs/health-checks + +**Terrafrom Mappings:** + +- `google_compute_health_check.name` + +## Supported Methods + +- `GET`: Get GCP Compute Health Check by "gcp-compute-health-check-name" +- `LIST`: List all GCP Compute Health Check items +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-http-health-check.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-http-health-check.md new file mode 100644 index 00000000..095d2e51 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-http-health-check.md @@ -0,0 +1,18 @@ +--- +title: GCP Compute Http Health Check +sidebar_label: gcp-compute-http-health-check +--- + +A **Google Cloud Compute HTTP Health Check** is a legacy, regional health-check resource that periodically issues HTTP `GET` requests to a specified path on your instances or load-balanced back-ends. If an instance responds with an acceptable status code (e.g. `200–299`) within the configured timeout for the required number of consecutive probes, it is marked healthy; otherwise, it is marked unhealthy. Load balancers and target pools use this signal to route traffic only to healthy instances, helping to maintain application availability. +Google now recommends the newer, unified _Health Check_ resource for most use-cases, but HTTP Health Checks remain fully supported and are still encountered in many estates. +For full details, see the official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/httpHealthChecks + +**Terrafrom Mappings:** + +- `google_compute_http_health_check.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-http-health-check by its "name" +- `LIST`: List all gcp-compute-http-health-check +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md new file mode 100644 index 00000000..77040607 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Image +sidebar_label: gcp-compute-image +--- + +A GCP Compute Image represents a bootable disk image in Google Compute Engine. Images capture the contents of a virtual machine’s root volume (operating system, installed packages, configuration files, etc.) and act as the template from which new persistent disks and VM instances are created. Teams use images to standardise the base operating-system layer across their fleet, speed up instance provisioning, and ensure consistency between environments. Modifying or deleting an image can therefore have an immediate impact on every workload that references it, including instance templates and managed instance groups. +Official documentation: https://cloud.google.com/compute/docs/images + +**Terrafrom Mappings:** + +- `google_compute_image.name` + +## Supported Methods + +- `GET`: Get GCP Compute Image by "gcp-compute-image-name" +- `LIST`: List all GCP Compute Image items +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group-manager.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group-manager.md new file mode 100644 index 00000000..bbe86356 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group-manager.md @@ -0,0 +1,34 @@ +--- +title: GCP Compute Instance Group Manager +sidebar_label: gcp-compute-instance-group-manager +--- + +A Google Cloud Compute Instance Group Manager is the control plane object that creates and maintains a Managed Instance Group (MIG). It provisions Virtual Machine (VM) instances from an Instance Template, keeps their number in line with the desired size, and automatically repairs or replaces unhealthy VMs to ensure uniformity across the group. In effect, it is the resource that makes a MIG self-healing and declarative. For full details see the official documentation: https://docs.cloud.google.com/compute/docs/reference/rest/v1/instanceGroupManagers. + +**Terrafrom Mappings:** + +- `google_compute_instance_group_manager.name` + +## Supported Methods + +- `GET`: Get GCP Compute Instance Group Manager by "gcp-compute-instance-group-manager-name" +- `LIST`: List all GCP Compute Instance Group Manager items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-autoscaler`](/sources/gcp/Types/gcp-compute-autoscaler) + +An autoscaler resource can reference a particular Instance Group Manager and adjust the group’s target size according to load metrics. When a link exists, Overmind shows which autoscaler is controlling the scaling behaviour of the MIG managed by this Instance Group Manager. + +### [`gcp-compute-instance-group`](/sources/gcp/Types/gcp-compute-instance-group) + +The Instance Group Manager owns and controls a specific managed instance group. This link reveals the underlying Instance Group object that represents the collection of VMs created by the manager. + +### [`gcp-compute-instance-template`](/sources/gcp/Types/gcp-compute-instance-template) + +Every Instance Group Manager specifies an Instance Template that defines the configuration of the VMs it will create (machine type, disks, metadata, etc.). Overmind links the manager to its template so you can trace configuration drift risks back to the source template. + +### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) + +When using legacy network load balancers, an Instance Group Manager may add its instances to one or more Target Pools. This link identifies the load-balancing back-ends that depend on the instances generated by the manager, helping to surface blast-radius considerations for networking changes. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group.md new file mode 100644 index 00000000..ee62276d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group.md @@ -0,0 +1,27 @@ +--- +title: GCP Compute Instance Group +sidebar_label: gcp-compute-instance-group +--- + +A Google Cloud Compute Instance Group is a logical collection of virtual machine (VM) instances that you manage as a single entity. Instance groups can be either managed (where the group is tied to an instance template and can perform auto-healing, autoscaling and rolling updates) or unmanaged (a simple grouping of individually created VMs). They are commonly used to distribute traffic across identical instances and to simplify operational tasks such as scaling and updates. +For an in-depth explanation, refer to the official documentation: https://cloud.google.com/compute/docs/instance-groups + +**Terrafrom Mappings:** + +- `google_compute_instance_group.name` + +## Supported Methods + +- `GET`: Get GCP Compute Instance Group by "gcp-compute-instance-group-name" +- `LIST`: List all GCP Compute Instance Group items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Each VM contained in the instance group is attached to a specific VPC network. Consequently, the instance group inherits a dependency on that GCP Compute Network; changes to the network (e.g., firewall rules, routing) can directly impact the availability or behaviour of all instances in the group. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Within its parent VPC network, every instance is placed in a particular subnetwork. Therefore, the instance group is transitively linked to the associated GCP Compute Subnetwork. Subnetwork configuration—such as IP ranges or regional placement—affects how the grouped instances communicate internally and with external resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-template.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-template.md new file mode 100644 index 00000000..fd742bd5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-template.md @@ -0,0 +1,63 @@ +--- +title: GCP Compute Instance Template +sidebar_label: gcp-compute-instance-template +--- + +A Compute Engine instance template is a reusable blueprint that captures almost all of the configuration needed to launch a Virtual Machine (VM) instance in Google Cloud: machine type, boot image, attached disks, network interfaces, metadata, service accounts, shielded-VM options and more. Templates allow you to create individual VM instances consistently or serve as the basis for managed instance groups that can scale automatically. +Official documentation: https://cloud.google.com/compute/docs/instance-templates + +**Terrafrom Mappings:** + +- `google_compute_instance_template.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-instance-template by its "name" +- `LIST`: List all gcp-compute-instance-template +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If customer-managed encryption keys (CMEK) are specified in the template, they reference a Cloud KMS crypto-key that will be used to encrypt the boot or data disks of any VM created from the template. + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +The template can define additional persistent disks to be auto-created and attached, or it can attach existing disks in read-only or read-write mode. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +The boot disk section of the template points to a Compute Engine image that is cloned each time a new VM is launched. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +When a user or an autoscaler instantiates the template, it materialises as one or more Compute Engine instances that inherit every property defined in the template. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every network interface defined in the template must belong to a VPC network, so the template contains links to the relevant network resources. + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +If the template targets sole-tenant nodes, it can specify a node group affinity so that all created VMs land on a particular node group. + +### [`gcp-compute-reservation`](/sources/gcp/Types/gcp-compute-reservation) + +Templates may be configured to consume capacity from an existing reservation, ensuring launched VMs fit within reserved resources. + +### [`gcp-compute-security-policy`](/sources/gcp/Types/gcp-compute-security-policy) + +Tags or service-account settings in the template can cause the resulting instances to match Cloud Armor security policies applied at the project or network level. + +### [`gcp-compute-snapshot`](/sources/gcp/Types/gcp-compute-snapshot) + +Instead of an image, the template can build new disks from a snapshot, linking the template to that snapshot resource. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +For networks that are in auto or custom subnet mode, the template points to the exact subnetwork each NIC should join. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +The template includes a service account and its OAuth scopes; the created VMs will assume that service account’s identity and permissions. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md new file mode 100644 index 00000000..ffa3f837 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md @@ -0,0 +1,30 @@ +--- +title: GCP Compute Instance +sidebar_label: gcp-compute-instance +--- + +A GCP Compute Instance is a virtual machine (VM) hosted on Google Cloud’s Compute Engine service. It provides configurable CPU, memory, storage and operating-system options, enabling you to run anything from small test services to large-scale production workloads. Instances can be created from public images or custom images, can have one or more network interfaces, and can attach multiple persistent or ephemeral disks. For full details see the official documentation: https://cloud.google.com/compute/docs/instances + +**Terrafrom Mappings:** + +- `google_compute_instance.name` + +## Supported Methods + +- `GET`: Get GCP Compute Instance by "gcp-compute-instance-name" +- `LIST`: List all GCP Compute Instance items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +A Compute Instance normally boots from and/or mounts one or more persistent disks. Overmind links an instance to every `gcp-compute-disk` that is attached to it so you can assess the impact of changes to those disks on the VM. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every network interface on a Compute Instance is connected to a VPC network. Overmind records this relationship to show how altering a `gcp-compute-network` (for example, changing routing or firewall rules) could affect the instance’s connectivity. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Within a VPC network, an interface resides in a specific subnetwork. Overmind links the instance to its `gcp-compute-subnetwork` so you can evaluate risks related to IP ranges, regional availability or subnet-level security policies that might influence the VM. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instant-snapshot.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instant-snapshot.md new file mode 100644 index 00000000..b26e582d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instant-snapshot.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Instant Snapshot +sidebar_label: gcp-compute-instant-snapshot +--- + +A GCP Compute Instant Snapshot is a point-in-time, crash-consistent copy of a persistent disk that is captured almost immediately, irrespective of the size of the disk. It is stored in the same region as the source disk and is intended for rapid backup, testing, or disaster-recovery scenarios where minimal creation time is essential. Instant snapshots are ephemeral by design (they are automatically deleted after seven days unless converted to a regular snapshot) and incur lower network egress because the data never leaves the region. +For full details, refer to the official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/instantSnapshots + +**Terrafrom Mappings:** + +- `google_compute_instant_snapshot.name` + +## Supported Methods + +- `GET`: Get GCP Compute Instant Snapshot by "gcp-compute-instant-snapshot-name" +- `LIST`: List all GCP Compute Instant Snapshot items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +An Instant Snapshot is created from a persistent disk. The snapshot’s `source_disk` field references the original `gcp-compute-disk`, and any restore or promotion operation will require access to that underlying disk or its region. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-machine-image.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-machine-image.md new file mode 100644 index 00000000..a8df0527 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-machine-image.md @@ -0,0 +1,35 @@ +--- +title: GCP Compute Machine Image +sidebar_label: gcp-compute-machine-image +--- + +A Google Cloud Compute Engine **Machine Image** is a first-class resource that stores all the information required to recreate one or more identical virtual machine instances, including boot and data disks, instance metadata, machine type, service accounts, and network interface definitions. Machine images make it easy to version-control complete VM templates and roll them out across projects or organisations. +Official documentation: https://cloud.google.com/compute/docs/machine-images + +**Terrafrom Mappings:** + +- `google_compute_machine_image.name` + +## Supported Methods + +- `GET`: Get GCP Compute Machine Image by "gcp-compute-machine-image-name" +- `LIST`: List all GCP Compute Machine Image items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +The machine image contains snapshots of every persistent disk that was attached to the source VM. Linking a machine image to its underlying disks allows Overmind to surface risks such as outdated disk encryption keys or insufficient replication settings. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +A machine image is normally created from, or used to instantiate, Compute Engine instances. Tracking this relationship lets you see which VMs were the origin of the image and which new VMs will inherit its configuration or vulnerabilities. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Network interface settings embedded in the machine image reference specific VPC networks. Connecting the image to those networks helps identify issues like deprecated network configurations that new VMs would inherit. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Each network interface in the machine image also specifies a subnetwork. Mapping this linkage highlights potential problems such as subnet IP exhaustion or mismatched IAM policies that could affect any instance launched from the image. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network-endpoint-group.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network-endpoint-group.md new file mode 100644 index 00000000..4377abfc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network-endpoint-group.md @@ -0,0 +1,34 @@ +--- +title: GCP Compute Network Endpoint Group +sidebar_label: gcp-compute-network-endpoint-group +--- + +A Google Cloud Compute Network Endpoint Group (NEG) is a collection of network endpoints—VM NICs, IP and port pairs, or fully-managed serverless targets such as Cloud Run and Cloud Functions—that you treat as a single backend for Google Cloud Load Balancing. By grouping endpoints into a NEG you can precisely steer traffic, perform health-checking, and scale back-end capacity without exposing individual resources. See the official documentation for full details: https://cloud.google.com/load-balancing/docs/negs/. + +**Terrafrom Mappings:** + +- `google_compute_network_endpoint_group.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-network-endpoint-group by its "name" +- `LIST`: List all gcp-compute-network-endpoint-group +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-functions-function`](/sources/gcp/Types/gcp-cloud-functions-function) + +Serverless NEGs can reference a Cloud Functions function as their target, allowing the function to serve as a backend to an HTTP(S) load balancer. Overmind links a NEG to the Cloud Functions function it fronts. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +A VM-based or hybrid NEG is created inside a specific VPC network; all its endpoints must belong to that network. Overmind therefore relates the NEG to the corresponding `gcp-compute-network`. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +For regional VM NEGs, each endpoint is an interface on a VM residing in a particular subnetwork. Overmind surfaces this dependency by linking the NEG to each associated `gcp-compute-subnetwork`. + +### [`gcp-run-service`](/sources/gcp/Types/gcp-run-service) + +When a Cloud Run service is exposed through an external HTTP(S) load balancer, Google automatically creates a serverless NEG representing that service. Overmind links the NEG back to its originating `gcp-run-service`. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md new file mode 100644 index 00000000..368d7337 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md @@ -0,0 +1,27 @@ +--- +title: GCP Compute Network +sidebar_label: gcp-compute-network +--- + +A Google Cloud VPC (Virtual Private Cloud) network is a global, logically-isolated network that spans all regions within a Google Cloud project. It defines the IP address space, routing tables, firewall rules and connectivity options (for example, VPN, Cloud Interconnect and peering) for the resources that are attached to it. Each VPC network can contain one or more regional subnetworks that allocate IP addresses to individual resources. +For a full description see the official Google Cloud documentation: https://cloud.google.com/vpc/docs/vpc. + +**Terrafrom Mappings:** + +- `google_compute_network.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-network by its "name" +- `LIST`: List all gcp-compute-network +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +A gcp-compute-network can be linked to another gcp-compute-network when the two are connected using VPC Network Peering. This relationship allows traffic to flow privately between the two VPC networks and is modelled in Overmind as a link between the respective network resources. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Each gcp-compute-network contains one or more gcp-compute-subnetwork resources. Overmind links a network to all of its subnetworks to show the hierarchy and to surface any risks that originate in the subnetwork configuration. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-group.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-group.md new file mode 100644 index 00000000..a5493f53 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-group.md @@ -0,0 +1,18 @@ +--- +title: GCP Compute Node Group +sidebar_label: gcp-compute-node-group +--- + +A **Google Cloud Compute Node Group** is a logical grouping of one or more sole-tenant nodes – dedicated physical Compute Engine servers that are exclusively reserved for your projects. Node groups let you manage the life-cycle, scheduling policies and placement of these nodes as a single resource. They are typically used when you need hardware isolation for licensing or security reasons, or when you require predictable performance unaffected by noisy neighbours. Each node in the group is created from a Node Template that defines the machine type, CPU platform, labels and maintenance behaviour for the nodes. +Official documentation: https://cloud.google.com/compute/docs/nodes/sole-tenant-nodes + +**Terrafrom Mappings:** + +- `google_compute_node_group.name` +- `google_compute_node_template.name` + +## Supported Methods + +- `GET`: Get GCP Compute Node Group by "gcp-compute-node-group-name" +- `LIST`: List all GCP Compute Node Group items +- `SEARCH`: Search for GCP Compute Node Group by "gcp-compute-node-group-nodeTemplateName" diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md new file mode 100644 index 00000000..11c1ab06 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Project +sidebar_label: gcp-compute-project +--- + +A Google Cloud project is the top-level, logical container for every resource you create in Google Cloud. It stores metadata such as billing configuration, IAM policy, APIs that are enabled, default network settings and quotas, and it provides an isolated namespace for resource names. In the context of Compute Engine, the project determines which VM instances, disks, firewalls and other compute resources can interact, and it is the unit against which most permissions and quotas are enforced. +Official documentation: https://cloud.google.com/resource-manager/docs/creating-managing-projects + +## Supported Methods + +- `GET`: Get a gcp-compute-project by its "name" +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every service account is created inside a single project and inherits that project’s IAM policy unless overridden. Overmind links a `gcp-compute-project` to the `gcp-iam-service-account` resources it owns so that you can trace how credentials and permissions propagate within the project. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Storage buckets live inside a project and consume that project’s quotas and billing account. Linking a `gcp-compute-project` to its `gcp-storage-bucket` resources lets you see which data stores are affected by changes to project-wide settings such as IAM roles or organisation policies. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-public-delegated-prefix.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-public-delegated-prefix.md new file mode 100644 index 00000000..32dd6369 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-public-delegated-prefix.md @@ -0,0 +1,27 @@ +--- +title: GCP Compute Public Delegated Prefix +sidebar_label: gcp-compute-public-delegated-prefix +--- + +A Google Cloud Compute Public Delegated Prefix represents a block of publicly-routable IPv4 or IPv6 addresses that Google has reserved and delegated to your project in a given region. Once the prefix exists you can further subdivide it into smaller delegated prefixes or assign individual addresses to resources such as VM instances, forwarding rules, or load balancers. Public Delegated Prefixes enable you to bring your own IP space, ensure predictable address allocation and control how traffic enters your network. +Official documentation: https://docs.cloud.google.com/vpc/docs/create-pdp + +**Terrafrom Mappings:** + +- `google_compute_public_delegated_prefix.id` + +## Supported Methods + +- `GET`: Get a gcp-compute-public-delegated-prefix by its "name" +- `LIST`: List all gcp-compute-public-delegated-prefix +- `SEARCH`: Search with full ID: projects/[project]/regions/[region]/publicDelegatedPrefixes/[name] (used for terraform mapping). + +## Possible Links + +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +A Public Delegated Prefix is created within, and therefore belongs to, a specific Cloud Resource Manager project. The project provides billing, IAM, and quota context for the prefix. + +### [`gcp-compute-public-delegated-prefix`](/sources/gcp/Types/gcp-compute-public-delegated-prefix) + +A Public Delegated Prefix can itself be the parent of smaller delegated prefixes; these child prefixes are represented by additional `gcp-compute-public-delegated-prefix` resources that reference the parent block. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-backend-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-backend-service.md new file mode 100644 index 00000000..4c64e0d0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-backend-service.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Region Backend Service +sidebar_label: gcp-compute-region-backend-service +--- + +A **GCP Compute Region Backend Service** is a regional load-balancing resource that defines how traffic is distributed to one or more back-end targets (such as Managed Instance Groups or Network Endpoint Groups) that all live in the same Google Cloud region. The service specifies settings such as the load-balancing protocol (HTTP, HTTPS, TCP, SSL etc.), session affinity, connection draining, health checks, fail-over behaviour and (optionally) Cloud Armor security policies. Regional backend services are used by Internal HTTP(S) Load Balancers, Internal TCP/UDP Load Balancers and several other Google Cloud load-balancing products. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/regionBackendServices + +**Terrafrom Mappings:** + +- `google_compute_region_backend_service.name` + +## Supported Methods + +- `GET`: Get GCP Compute Region Backend Service by "gcp-compute-region-backend-service-name" +- `LIST`: List all GCP Compute Region Backend Service items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-instance-group`](/sources/gcp/Types/gcp-compute-instance-group) + +A region backend service lists one or more Managed Instance Groups (or unmanaged instance groups) as its back-ends; the load balancer distributes traffic across the VMs contained in these instance groups. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +For internal load balancing, the region backend service is tied to a specific VPC network. All back-ends must reside in subnets that belong to this network and traffic from the forwarding rule is delivered through it. + +### [`gcp-compute-security-policy`](/sources/gcp/Types/gcp-compute-security-policy) + +A backend service can optionally reference a Cloud Armor security policy. When attached, that policy governs and filters incoming requests before they reach the back-end targets. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-commitment.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-commitment.md new file mode 100644 index 00000000..345b2e62 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-commitment.md @@ -0,0 +1,22 @@ +--- +title: GCP Compute Region Commitment +sidebar_label: gcp-compute-region-commitment +--- + +A GCP Compute Region Commitment is an agreement in which you purchase a predefined amount of vCPU, memory or GPU capacity in a specific region for a fixed term (one or three years) in return for a reduced hourly price. Commitments are applied automatically to matching usage within the chosen region, helping to lower running costs while guaranteeing a baseline level of capacity. For a detailed explanation of the feature, see the official documentation: https://docs.cloud.google.com/compute/docs/reference/rest/v1/regionCommitments/list. + +**Terrafrom Mappings:** + +- `google_compute_region_commitment.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-region-commitment by its "name" +- `LIST`: List all gcp-compute-region-commitment +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-reservation`](/sources/gcp/Types/gcp-compute-reservation) + +A region commitment can be consumed by one or more compute reservations in the same region. When a reservation launches virtual machine instances, the resources they use are first drawn from any applicable commitments so that the discounted commitment pricing is applied automatically. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md new file mode 100644 index 00000000..446cca00 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md @@ -0,0 +1,22 @@ +--- +title: GCP Compute Reservation +sidebar_label: gcp-compute-reservation +--- + +A GCP Compute Reservation is a zonal reservation of Compute Engine capacity that guarantees the availability of a specific machine type (and, optionally, attached GPUs, local SSDs, etc.) for when you later launch virtual machine (VM) instances. By pre-allocating vCPU and memory resources, reservations help you avoid capacity-related scheduling failures in busy zones and can be shared across projects inside the same organisation if desired. See the official documentation for full details: https://docs.cloud.google.com/compute/docs/instances/reservations-overview. + +**Terrafrom Mappings:** + +- `google_compute_reservation.name` + +## Supported Methods + +- `GET`: Get GCP Compute Reservation by "gcp-compute-reservation-name" +- `LIST`: List all GCP Compute Reservation items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-region-commitment`](/sources/gcp/Types/gcp-compute-region-commitment) + +Capacity held by a reservation counts against any existing regional commitment in the same region. By linking a reservation to its corresponding `gcp-compute-region-commitment`, you can see whether the reserved resources are already discounted or whether additional commitments may be required. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md new file mode 100644 index 00000000..3cbe6af4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Route +sidebar_label: gcp-compute-route +--- + +A GCP Compute Route is an entry in the routing table of a Google Cloud VPC network that determines how packets are forwarded from its subnets. Each route specifies a destination CIDR block and a next hop (for example, an instance, VPN tunnel, gateway, or peered network). Custom routes can be created to direct traffic through specific appliances, across VPNs, or towards on-premises networks, while system-generated routes provide default Internet and subnet behaviour. +See the official documentation for full details: https://cloud.google.com/vpc/docs/routes + +**Terrafrom Mappings:** + +- `google_compute_route.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-route by its "name" +- `LIST`: List all gcp-compute-route +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +If `next_hop_instance` is set, the route forwards matching traffic to the specified VM instance. Overmind therefore links the route to that Compute Instance, as deleting or modifying the instance will break the route. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every route belongs to exactly one VPC network, referenced in the `network` field. The network’s routing table is the context in which the route operates, so Overmind links the route to its parent network. + +### [`gcp-compute-vpn-tunnel`](/sources/gcp/Types/gcp-compute-vpn-tunnel) + +When `next_hop_vpn_tunnel` is used, the route sends traffic into a specific VPN tunnel. This dependency is captured by linking the route to the corresponding Compute VPN Tunnel, since changes to the tunnel affect the route’s viability. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md new file mode 100644 index 00000000..107de7f2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Router +sidebar_label: gcp-compute-router +--- + +A Google Cloud **Compute Router** is a regional, fully distributed control-plane resource that learns and exchanges dynamic routes between your Virtual Private Cloud (VPC) network and on-premises or partner networks. It implements the Border Gateway Protocol (BGP) on your behalf, allowing Cloud VPN tunnels and Cloud Interconnect attachments (VLANs) to advertise and receive custom routes without manual updates. Compute Routers are attached to a specific VPC network and region, but they propagate learned routes across the entire VPC through Google’s global backbone. +For a comprehensive overview, refer to the official Google Cloud documentation: https://cloud.google.com/network-connectivity/docs/router/how-to/creating-routers + +**Terrafrom Mappings:** + +- `google_compute_router.id` + +## Supported Methods + +- `GET`: Get a gcp-compute-router by its "name" +- `LIST`: List all gcp-compute-router +- `SEARCH`: Search with full ID: projects/[project]/regions/[region]/routers/[router] (used for terraform mapping). + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every Compute Router is created inside a particular VPC network; the router exchanges routes on behalf of that network. Therefore, a gcp-compute-router will always have an owning gcp-compute-network. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Subnets define the IP ranges that the Compute Router ultimately advertises (or learns routes for) within the VPC. Routes learned or propagated by the router directly affect traffic flowing to and from gcp-compute-subnetwork resources. + +### [`gcp-compute-vpn-tunnel`](/sources/gcp/Types/gcp-compute-vpn-tunnel) + +Compute Routers terminate the BGP sessions used by Cloud VPN (HA VPN) tunnels. Each gcp-compute-vpn-tunnel can be configured to peer with a Compute Router interface, enabling dynamic route exchange between the tunnel and the VPC. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-security-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-security-policy.md new file mode 100644 index 00000000..e4013647 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-security-policy.md @@ -0,0 +1,18 @@ +--- +title: GCP Compute Security Policy +sidebar_label: gcp-compute-security-policy +--- + +A GCP Compute Security Policy represents a Cloud Armor security policy. It contains an ordered set of layer-7 filtering rules that allow, deny, or rate-limit traffic directed at a load balancer or backend service. By attaching a security policy you can enforce web-application-firewall (WAF) protections, mitigate DDoS attacks, and define custom match conditions—all without changing your application code. Overmind ingests these resources so you can understand how proposed changes will affect the exposure and resilience of your workloads before you deploy them. + +For full details see the official Google Cloud documentation: https://cloud.google.com/armor/docs/security-policy-concepts + +**Terrafrom Mappings:** + +- `google_compute_security_policy.name` + +## Supported Methods + +- `GET`: Get GCP Compute Security Policy by "gcp-compute-security-policy-name" +- `LIST`: List all GCP Compute Security Policy items +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md new file mode 100644 index 00000000..2dba8fa9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md @@ -0,0 +1,27 @@ +--- +title: GCP Compute Snapshot +sidebar_label: gcp-compute-snapshot +--- + +A GCP Compute Snapshot is a point-in-time, incremental backup of a Compute Engine persistent disk. Snapshots allow you to restore data following accidental deletion, corruption, or regional outage, and can also be used to create new disks in the same or a different project/region. Because snapshots are incremental, only the blocks that have changed since the last snapshot are stored, reducing cost and network egress. Snapshots can be scheduled, encrypted with customer-managed keys, and shared across projects through Cloud Storage-backed snapshot storage. +Official documentation: https://cloud.google.com/compute/docs/disks/snapshots + +**Terrafrom Mappings:** + +- `google_compute_snapshot.name` + +## Supported Methods + +- `GET`: Get GCP Compute Snapshot by "gcp-compute-snapshot-name" +- `LIST`: List all GCP Compute Snapshot items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +A snapshot is created from a specific persistent disk; the link lets you trace a snapshot back to the disk it protects, or discover all snapshots derived from that disk. + +### [`gcp-compute-instant-snapshot`](/sources/gcp/Types/gcp-compute-instant-snapshot) + +An instant snapshot can later be converted into a standard snapshot, or serve as an intermediary during a snapshot operation. This link shows lineage between an instant snapshot and the resulting persistent snapshot resource. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-certificate.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-certificate.md new file mode 100644 index 00000000..350c08be --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-certificate.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Ssl Certificate +sidebar_label: gcp-compute-ssl-certificate +--- + +A GCP Compute SSL Certificate is a regional resource that stores the public and private key material required to terminate TLS for Google Cloud load balancers and proxy targets. Once created, the certificate can be attached to target HTTPS proxies (for external HTTP(S) Load Balancing) or target SSL proxies (for SSL Proxy Load Balancing) so that incoming connections can be securely encrypted in transit. Certificate data is provided by the user (self-managed) and can later be rotated or deleted as required. +For full details see the Google Cloud documentation: https://cloud.google.com/compute/docs/reference/rest/v1/sslCertificates + +**Terrafrom Mappings:** + +- `google_compute_ssl_certificate.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-ssl-certificate by its "name" +- `LIST`: List all gcp-compute-ssl-certificate +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-policy.md new file mode 100644 index 00000000..020ac502 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-policy.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Ssl Policy +sidebar_label: gcp-compute-ssl-policy +--- + +Google Cloud SSL policies allow you to define which TLS protocol versions and cipher suites can be used when clients negotiate secure connections with Google Cloud load balancers. By attaching an SSL policy to an HTTPS, SSL, or TCP proxy load balancer, you can enforce modern cryptographic standards, disable deprecated protocols, or maintain compatibility with legacy clients, thereby controlling the security posture of your services. Overmind can surface potential risks—such as the continued availability of weak ciphers—before you deploy. +For more information, see the official Google Cloud documentation: [SSL policies overview](https://cloud.google.com/compute/docs/reference/rest/v1/sslPolicies/get). + +**Terrafrom Mappings:** + +- `google_compute_ssl_policy.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-ssl-policy by its "name" +- `LIST`: List all gcp-compute-ssl-policy +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md new file mode 100644 index 00000000..00e54145 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md @@ -0,0 +1,22 @@ +--- +title: GCP Compute Subnetwork +sidebar_label: gcp-compute-subnetwork +--- + +A GCP Compute Subnetwork is a regional segment of a Virtual Private Cloud (VPC) network that defines an IP address range from which resources such as VM instances, GKE nodes, and internal load balancers receive their internal IP addresses. Each subnetwork is bound to a single region, can be configured for automatic or custom IP allocation, and supports features such as Private Google Access and flow logs. For full details see the official Google Cloud documentation: https://cloud.google.com/vpc/docs/subnets + +**Terrafrom Mappings:** + +- `google_compute_subnetwork.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-subnetwork by its "name" +- `LIST`: List all gcp-compute-subnetwork +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every subnetwork is a child resource of a VPC network. The `gcp-compute-network` item represents that parent VPC; a single network can contain multiple subnetworks, while each subnetwork is associated with exactly one network. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-http-proxy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-http-proxy.md new file mode 100644 index 00000000..980032c0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-http-proxy.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Target Http Proxy +sidebar_label: gcp-compute-target-http-proxy +--- + +A Google Cloud Compute Target HTTP Proxy acts as the intermediary between a forwarding rule and your defined URL map. When an incoming request reaches the load balancer, the proxy evaluates the host and path rules in the URL map and then forwards the request to the selected backend service. In essence, it is the control point that translates external client traffic into internal service calls, supporting features such as global anycast IPs, health-checking, and intelligent request routing for high-availability web applications. +For further information, see the official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/targetHttpProxies + +**Terrafrom Mappings:** + +- `google_compute_target_http_proxy.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-target-http-proxy by its "name" +- `LIST`: List all gcp-compute-target-http-proxy +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-https-proxy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-https-proxy.md new file mode 100644 index 00000000..1f9a673c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-https-proxy.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Target Https Proxy +sidebar_label: gcp-compute-target-https-proxy +--- + +A **Target HTTPS Proxy** is a global Google Cloud resource that terminates incoming HTTPS traffic and forwards the decrypted requests to the appropriate backend service according to a referenced URL map. It is a central component of the External HTTP(S) Load Balancer, holding one or more SSL certificates that are presented to clients during the TLS handshake and optionally enforcing an SSL policy that dictates the allowed protocol versions and cipher suites. +Official documentation: https://docs.cloud.google.com/sdk/gcloud/reference/compute/target-https-proxies + +**Terrafrom Mappings:** + +- `google_compute_target_https_proxy.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-target-https-proxy by its "name" +- `LIST`: List all gcp-compute-target-https-proxy +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-ssl-certificate`](/sources/gcp/Types/gcp-compute-ssl-certificate) + +The proxy references one or more SSL certificates that are served to clients when they initiate an HTTPS connection. These certificates are specified in the `ssl_certificates` field of the target HTTPS proxy. + +### [`gcp-compute-ssl-policy`](/sources/gcp/Types/gcp-compute-ssl-policy) + +An optional SSL policy can be attached to the proxy to control minimum TLS versions, allowed cipher suites, and other security settings. The policy is linked through the `ssl_policy` attribute. + +### [`gcp-compute-url-map`](/sources/gcp/Types/gcp-compute-url-map) + +Each target HTTPS proxy must reference exactly one URL map, which defines the routing rules that determine which backend service receives each request after SSL/TLS termination. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-pool.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-pool.md new file mode 100644 index 00000000..b7b49d1b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-pool.md @@ -0,0 +1,30 @@ +--- +title: GCP Compute Target Pool +sidebar_label: gcp-compute-target-pool +--- + +A Compute Target Pool is a regional resource that groups multiple VM instances so they can receive incoming traffic from legacy network TCP load balancers or be used as failover targets for forwarding rules. Target pools can also be linked to one or more Health Checks to determine the availability of their member instances. Official documentation: https://docs.cloud.google.com/load-balancing/docs/target-pools + +**Terrafrom Mappings:** + +- `google_compute_target_pool.id` + +## Supported Methods + +- `GET`: Get a gcp-compute-target-pool by its "name" +- `LIST`: List all gcp-compute-target-pool +- `SEARCH`: Search with full ID: projects/[project]/regions/[region]/targetPools/[name] (used for terraform mapping). + +## Possible Links + +### [`gcp-compute-health-check`](/sources/gcp/Types/gcp-compute-health-check) + +A target pool may reference one or more Health Checks. These checks are executed against each instance in the pool to decide whether the instance should receive traffic. Overmind links a target pool to any health check resources it is configured to use. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +Member virtual machines are registered in the target pool. Overmind establishes links from the target pool to every compute instance that is currently part of the pool. + +### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) + +Target pools can appear as dependencies of other target pools in scenarios such as cross-region failover configurations. Overmind represents these intra-type relationships with links between the relevant target pool resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-url-map.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-url-map.md new file mode 100644 index 00000000..40025ed3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-url-map.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Url Map +sidebar_label: gcp-compute-url-map +--- + +A Google Cloud Platform (GCP) Compute URL Map is the routing table used by an External or Internal HTTP(S) Load Balancer. It evaluates the host and path of each incoming request and, according to the host rules and path matchers you configure, forwards that request to the appropriate backend service or backend bucket. In other words, the URL map determines “which traffic goes where” once it reaches the load balancer, making it a critical part of any web-facing deployment. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/urlMaps + +**Terrafrom Mappings:** + +- `google_compute_url_map.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-url-map by its "name" +- `LIST`: List all gcp-compute-url-map +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-backend-service`](/sources/gcp/Types/gcp-compute-backend-service) + +Each URL map references one or more backend services in its path-matcher rules. Overmind therefore creates outbound links from a `gcp-compute-url-map` to every `gcp-compute-backend-service` that might receive traffic, allowing you to trace the full request path and identify downstream risks. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-gateway.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-gateway.md new file mode 100644 index 00000000..b51f2540 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-gateway.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Vpn Gateway +sidebar_label: gcp-compute-vpn-gateway +--- + +A Google Cloud Compute VPN Gateway (specifically, the High-Availability VPN Gateway) provides a managed, highly available IPsec VPN endpoint that allows encrypted traffic to flow between a Google Cloud Virtual Private Cloud (VPC) network and an on-premises network or another cloud provider. By deploying a VPN Gateway you can create site-to-site tunnels that automatically scale their throughput and offer automatic fail-over across two interfaces in different zones within the same region. +For full details see the official documentation: https://cloud.google.com/network-connectivity/docs/vpn/concepts/overview + +**Terrafrom Mappings:** + +- `google_compute_ha_vpn_gateway.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-vpn-gateway by its "name" +- `LIST`: List all gcp-compute-vpn-gateway +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +An HA VPN Gateway is created inside, and tightly bound to, a specific VPC network and region. It inherits the network’s subnet routes and advertises them across its VPN tunnels, and all incoming VPN traffic is delivered into that network. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-tunnel.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-tunnel.md new file mode 100644 index 00000000..ddd9908b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-tunnel.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Vpn Tunnel +sidebar_label: gcp-compute-vpn-tunnel +--- + +A **GCP Compute VPN Tunnel** represents a single IPSec tunnel that is part of a Cloud VPN connection. It contains the parameters needed to establish and maintain the encrypted link – peer IP address, shared secret, IKE version, traffic selectors, and the attachment to either a Classic VPN gateway or an HA VPN gateway. In most deployments two or more tunnels are created for redundancy. +For the full specification see the official Google documentation: https://cloud.google.com/compute/docs/reference/rest/v1/vpnTunnels + +**Terrafrom Mappings:** + +- `google_compute_vpn_tunnel.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-vpn-tunnel by its "name" +- `LIST`: List all gcp-compute-vpn-tunnel +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-external-vpn-gateway`](/sources/gcp/Types/gcp-compute-external-vpn-gateway) + +When the tunnel terminates on equipment outside Google Cloud, the `externalVpnGateway` field is set. This creates a relationship between the VPN tunnel and the corresponding External VPN Gateway resource. + +### [`gcp-compute-router`](/sources/gcp/Types/gcp-compute-router) + +If dynamic routing is enabled (HA VPN or dynamic Classic VPN), the tunnel is attached to a Cloud Router, which advertises and learns routes via BGP. The `router` field therefore links the VPN tunnel to a specific Cloud Router. + +### [`gcp-compute-vpn-gateway`](/sources/gcp/Types/gcp-compute-vpn-gateway) + +Every tunnel belongs to a Google-managed VPN gateway (`targetVpnGateway` for Classic VPN or `vpnGateway` for HA VPN). This link captures that parent-child relationship, allowing Overmind to evaluate the impact of gateway changes on its tunnels. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md new file mode 100644 index 00000000..eed12aa9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md @@ -0,0 +1,47 @@ +--- +title: GCP Container Cluster +sidebar_label: gcp-container-cluster +--- + +Google Kubernetes Engine (GKE) Container Clusters provide managed Kubernetes control-planes and node infrastructure on Google Cloud Platform. A cluster groups together one or more node pools running containerised workloads, and exposes both the Kubernetes API server and optional add-ons such as Cloud Monitoring, Cloud Logging, Workload Identity and Binary Authorisation. +For a full description of the service see the official Google documentation: https://cloud.google.com/kubernetes-engine/docs + +**Terrafrom Mappings:** + +- `google_container_cluster.id` + +## Supported Methods + +- `GET`: Get a gcp-container-cluster by its "locations|clusters" +- ~~`LIST`~~ +- `SEARCH`: Search for GKE clusters in a location. Use the format "location" or the full resource name supported for terraform mappings. + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A cluster can be configured to encrypt Kubernetes secrets and etcd data at rest using a customer-managed Cloud KMS crypto key. When customer-managed encryption is enabled, the cluster stores the resource ID of the key that protects its control-plane data, creating a link between the cluster and the KMS crypto key. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every GKE cluster is deployed into a VPC network. All control-plane and node traffic flows inside this network, and the cluster stores the name of the network it belongs to, creating a relationship with the corresponding gcp-compute-network resource. + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +If a node pool is configured to run on sole-tenant nodes, GKE provisions or attaches to Compute Engine node groups for placement. The cluster will therefore reference any node groups used by its node pools. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Within the chosen VPC, a cluster is attached to one or more subnetworks to allocate IP ranges for nodes, pods and services. The subnetwork resource(s) appear in the cluster’s configuration and are linked to the cluster. + +### [`gcp-container-node-pool`](/sources/gcp/Types/gcp-container-node-pool) + +A cluster is composed of one or more node pools that provide the actual worker nodes. Each node pool references its parent cluster, and the cluster maintains a list of all associated node pools. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +GKE uses service accounts for both the control-plane (Google-managed) and the nodes (user-specified or default). Additionally, Workload Identity maps Kubernetes service accounts to IAM service accounts. Any service account configured for node pools, Workload Identity or authorised networks will be linked to the cluster. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +Audit logs and event streams originating from a GKE cluster can be exported via Logging sinks to Pub/Sub topics for downstream processing. When such a sink targets a Pub/Sub topic, the cluster indirectly references that topic, creating a link captured by Overmind. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-node-pool.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-node-pool.md new file mode 100644 index 00000000..dff4c40d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-node-pool.md @@ -0,0 +1,35 @@ +--- +title: GCP Container Node Pool +sidebar_label: gcp-container-node-pool +--- + +A Google Cloud Platform (GCP) Container Node Pool is a logical grouping of worker nodes within a Google Kubernetes Engine (GKE) cluster. All nodes in a pool share the same configuration (machine type, disk size, metadata, labels, etc.) and are managed as a single unit for operations such as upgrades, autoscaling and maintenance. Node pools allow you to mix and match node types inside a single cluster, enabling workload-specific optimisation, cost control and security hardening. +Official documentation: https://cloud.google.com/kubernetes-engine/docs/concepts/node-pools + +**Terrafrom Mappings:** + +- `google_container_node_pool.id` + +## Supported Methods + +- `GET`: Get a gcp-container-node-pool by its "locations|clusters|nodePools" +- ~~`LIST`~~ +- `SEARCH`: Search GKE Node Pools within a cluster. Use "[location]|[cluster]" or the full resource name supported by Terraform mappings: "[project]/[location]/[cluster]/[node_pool_name]" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A node pool can be configured to use a Cloud KMS CryptoKey for at-rest encryption of node boot disks or customer-managed encryption keys (CMEK) for GKE secrets. Overmind links the node pool to the KMS key that protects its data, allowing you to trace encryption dependencies. + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +When a node pool is created on sole-tenant nodes, GKE provisions the underlying Compute Engine Node Group that hosts those VMs. Linking highlights which Node Group provides the physical tenancy for the pool’s nodes. + +### [`gcp-container-cluster`](/sources/gcp/Types/gcp-container-cluster) + +Every node pool belongs to exactly one GKE cluster. This parent-child relationship is surfaced so you can quickly navigate from a pool to its cluster and understand cluster-level configuration and risk. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Each VM in a node pool runs as an IAM service account (often the “default” compute service account or a custom node service account). Overmind links the pool to that service account to expose permissions granted to workloads running on the nodes. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md new file mode 100644 index 00000000..dccaedcb --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md @@ -0,0 +1,31 @@ +--- +title: GCP Dataform Repository +sidebar_label: gcp-dataform-repository +--- + +A GCP Dataform Repository is the top-level, version-controlled container that stores all the SQL workflow code, configuration files and commit history used by Dataform in Google Cloud. It functions much like a Git repository, allowing data teams to develop, test and deploy BigQuery pipelines through branches, pull requests and releases. Repositories live under a specific project and location and can be connected to Cloud Source Repositories or external Git providers. +Official documentation: https://cloud.google.com/dataform/docs/repositories + +**Terrafrom Mappings:** + +- `google_dataform_repository.id` + +## Supported Methods + +- `GET`: Get a gcp-dataform-repository by its "locations|repositories" +- ~~`LIST`~~ +- `SEARCH`: Search for Dataform repositories in a location. Use the format "location" or "projects/[project_id]/locations/[location]/repositories/[repository_name]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If Customer-Managed Encryption Keys (CMEK) are enabled for the repository, it contains a reference to the Cloud KMS crypto key that encrypts its metadata. Overmind follows this link to verify key existence, rotation policy and wider blast radius. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Dataform executes queries and workflow steps using a service account specified in the repository or workspace settings. Linking to the IAM service account lets Overmind trace which identities can act on behalf of the repository and assess permission risks. + +### [`gcp-secret-manager-secret`](/sources/gcp/Types/gcp-secret-manager-secret) + +A repository may reference secrets (such as connection strings or API tokens) stored in Secret Manager via environment variables or workflow configurations. Overmind links to these secrets to ensure they exist, are properly protected and are not about to be rotated or deleted. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-aspect-type.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-aspect-type.md new file mode 100644 index 00000000..af6be780 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-aspect-type.md @@ -0,0 +1,17 @@ +--- +title: GCP Dataplex Aspect Type +sidebar_label: gcp-dataplex-aspect-type +--- + +A Google Cloud Dataplex Aspect Type is a reusable template that describes the structure and semantics of a particular piece of metadata—an _aspect_—that can later be attached to Dataplex assets, entries, or partitions. By defining aspect types centrally, an organisation can guarantee that the same metadata schema (for example, “Personally Identifiable Information classification” or “Data-quality score”) is applied consistently across lakes, zones, and assets, thereby strengthening governance, lineage, and discovery capabilities. +For further details, see the official Dataplex REST reference: https://cloud.google.com/dataplex/docs/reference/rest/v1/projects.locations.aspectTypes + +**Terrafrom Mappings:** + +- `google_dataplex_aspect_type.id` + +## Supported Methods + +- `GET`: Get a gcp-dataplex-aspect-type by its "locations|aspectTypes" +- ~~`LIST`~~ +- `SEARCH`: Search for Dataplex aspect types in a location. Use the format "location" or "projects/[project_id]/locations/[location]/aspectTypes/[aspect_type_id]" which is supported for terraform mappings. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-data-scan.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-data-scan.md new file mode 100644 index 00000000..a9caa00a --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-data-scan.md @@ -0,0 +1,22 @@ +--- +title: GCP Dataplex Data Scan +sidebar_label: gcp-dataplex-data-scan +--- + +A Dataplex Data Scan is a managed resource that schedules and executes automated profiling or data-quality checks over data held in Google Cloud Platform (GCP) storage systems such as Cloud Storage and BigQuery. The scan stores its configuration, execution history and results, allowing teams to understand the structure, completeness and validity of their datasets before those datasets are used downstream. Full details can be found in the official Google Cloud documentation: https://docs.cloud.google.com/dataplex/docs/use-data-profiling + +**Terrafrom Mappings:** + +- `google_dataplex_datascan.id` + +## Supported Methods + +- `GET`: Get a gcp-dataplex-data-scan by its "locations|dataScans" +- ~~`LIST`~~ +- `SEARCH`: Search for Dataplex data scans in a location. Use the location name e.g., 'us-central1' or the format "projects/[project_id]/locations/[location]/dataScans/[data_scan_id]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +A Dataplex Data Scan can target objects stored in a Cloud Storage bucket for profiling or quality validation. Therefore, Overmind links the scan resource to the bucket that contains the underlying data being analysed, enabling a complete view of the data-quality pipeline and its dependencies. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-entry-group.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-entry-group.md new file mode 100644 index 00000000..04d5e81f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-entry-group.md @@ -0,0 +1,17 @@ +--- +title: GCP Dataplex Entry Group +sidebar_label: gcp-dataplex-entry-group +--- + +A Dataplex Entry Group is a logical container in Google Cloud that lives in the Data Catalog service and is used by Dataplex to organise metadata about datasets, tables and other data assets. By grouping related Data Catalog entries together, Entry Groups enable consistent discovery, governance and lineage tracking across lakes, zones and projects. Each Entry Group is created in a specific project and location and can be referenced by Dataplex jobs, policies and fine-grained IAM settings. +For full details see Google’s REST reference: https://cloud.google.com/data-catalog/docs/reference/rest/v1/projects.locations.entryGroups + +**Terrafrom Mappings:** + +- `google_dataplex_entry_group.id` + +## Supported Methods + +- `GET`: Get a gcp-dataplex-entry-group by its "locations|entryGroups" +- ~~`LIST`~~ +- `SEARCH`: Search for Dataplex entry groups in a location. Use the format "location" or "projects/[project_id]/locations/[location]/entryGroups/[entry_group_id]" which is supported for terraform mappings. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-autoscaling-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-autoscaling-policy.md new file mode 100644 index 00000000..9532af2f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-autoscaling-policy.md @@ -0,0 +1,16 @@ +--- +title: GCP Dataproc Autoscaling Policy +sidebar_label: gcp-dataproc-autoscaling-policy +--- + +A Google Cloud Dataproc Autoscaling Policy defines how a Dataproc cluster should automatically grow or shrink its worker and secondary-worker (pre-emptible) node groups in response to load. Policies specify minimum and maximum instance counts, cooldown periods, and scaling rules based on YARN memory or CPU utilisation, allowing clusters to meet workload demand while controlling cost. Once created at the project or region level, a policy can be referenced by any Dataproc cluster in that location. For more detail see the official documentation: https://cloud.google.com/dataproc/docs/concepts/configuring-clusters/autoscaling. + +**Terrafrom Mappings:** + +- `google_dataproc_autoscaling_policy.name` + +## Supported Methods + +- `GET`: Get a gcp-dataproc-autoscaling-policy by its "name" +- `LIST`: List all gcp-dataproc-autoscaling-policy +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md new file mode 100644 index 00000000..23805167 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md @@ -0,0 +1,54 @@ +--- +title: GCP Dataproc Cluster +sidebar_label: gcp-dataproc-cluster +--- + +A Google Cloud Dataproc Cluster is a managed cluster of Compute Engine virtual machines that runs open-source data-processing frameworks such as Apache Spark, Apache Hadoop, Presto and Trino. Dataproc handles the provisioning, configuration and ongoing management of the cluster, allowing you to submit jobs or create ephemeral clusters on demand while paying only for the compute you use. For full feature details see the official documentation: https://docs.cloud.google.com/dataproc/docs/concepts/overview. + +**Terrafrom Mappings:** + +- `google_dataproc_cluster.name` + +## Supported Methods + +- `GET`: Get a gcp-dataproc-cluster by its "name" +- `LIST`: List all gcp-dataproc-cluster +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Dataproc cluster can be configured to use a customer-managed encryption key (CMEK) from Cloud KMS to encrypt the persistent disks attached to its nodes as well as the cluster’s Cloud Storage staging bucket. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +Each Dataproc cluster is built from a specific Dataproc image (e.g., `2.1-debian11`). The image determines the operating system and the versions of Hadoop, Spark and other components installed on the VM instances. + +### [`gcp-compute-instance-group-manager`](/sources/gcp/Types/gcp-compute-instance-group-manager) + +Behind the scenes Dataproc creates managed instance groups for the primary, secondary and optional pre-emptible worker node pools. These MIGs handle instance creation, health-checking and replacement. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +The cluster’s VMs are attached to a specific VPC network, determining their routability and ability to reach other Google Cloud services or on-premises systems. + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +If you run Dataproc on sole-tenant nodes, the cluster associates each VM with a Compute Node Group to guarantee dedicated physical hardware. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Within the chosen VPC, the cluster can be pinned to a particular subnetwork to control IP address ranges, firewall rules and routing. + +### [`gcp-dataproc-autoscaling-policy`](/sources/gcp/Types/gcp-dataproc-autoscaling-policy) + +Clusters may reference an Autoscaling Policy that automatically adds or removes worker nodes based on YARN or Spark metrics, optimising performance and cost. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Dataproc node runs under a Compute Engine service account. This account’s IAM roles determine the cluster’s permission to read/write Cloud Storage, publish metrics, access BigQuery, etc. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Dataproc uses Cloud Storage buckets for staging job files, storing cluster logs and optionally as a default HDFS replacement via the `gcs://` connector. The cluster therefore references one or more buckets during its lifecycle. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dns-managed-zone.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dns-managed-zone.md new file mode 100644 index 00000000..7f296db9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dns-managed-zone.md @@ -0,0 +1,27 @@ +--- +title: GCP Dns Managed Zone +sidebar_label: gcp-dns-managed-zone +--- + +A Cloud DNS Managed Zone is a logical container within Google Cloud that holds the DNS records for a particular namespace (for example, `example.com`). Each managed zone is served by a set of authoritative name servers and can be either public (resolvable on the public internet) or private (resolvable only from selected VPC networks). Managed zones let you create, update, and delete DNS resource-record sets using the Cloud DNS API, gcloud CLI, or Terraform. +For full details see Google’s documentation: https://docs.cloud.google.com/dns/docs/zones + +**Terrafrom Mappings:** + +- `google_dns_managed_zone.name` + +## Supported Methods + +- `GET`: Get a gcp-dns-managed-zone by its "name" +- `LIST`: List all gcp-dns-managed-zone +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Private managed zones can be attached to one or more VPC networks. When such a link exists, DNS queries originating from resources inside the referenced `gcp-compute-network` are resolved using the records defined in the managed zone. Overmind surfaces this relationship to show which networks will be affected by changes to the zone’s records or visibility settings. + +### [`gcp-container-cluster`](/sources/gcp/Types/gcp-container-cluster) + +Google Kubernetes Engine may automatically create or rely on Cloud DNS managed zones for features such as service discovery, Cloud DNS-based Pod/Service FQDN resolution, or workload identity federation. Linking a `gcp-dns-managed-zone` to a `gcp-container-cluster` allows Overmind to highlight how DNS adjustments could impact cluster-internal name resolution or ingress behaviour for that cluster. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-essential-contacts-contact.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-essential-contacts-contact.md new file mode 100644 index 00000000..d3797d1d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-essential-contacts-contact.md @@ -0,0 +1,17 @@ +--- +title: GCP Essential Contacts Contact +sidebar_label: gcp-essential-contacts-contact +--- + +Google Cloud’s Essential Contacts service allows an organisation to register one or more e-mail addresses that will receive important operational and security notifications about a project, folder, or organisation. A “contact” resource represents a single recipient and records the e-mail address, preferred language and notification categories that the person should receive. More than one contact can be added so that the right teams are informed whenever Google issues mandatory or time-sensitive messages. +For a full description of the resource and its fields, refer to the official documentation: https://cloud.google.com/resource-manager/docs/managing-notification-contacts + +**Terrafrom Mappings:** + +- `google_essential_contacts_contact.id` + +## Supported Methods + +- `GET`: Get a gcp-essential-contacts-contact by its "name" +- `LIST`: List all gcp-essential-contacts-contact +- `SEARCH`: Search for contacts by their ID in the form of "projects/[project_id]/contacts/[contact_id]". diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md new file mode 100644 index 00000000..f9a224ba --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md @@ -0,0 +1,27 @@ +--- +title: GCP File Instance +sidebar_label: gcp-file-instance +--- + +A GCP Filestore instance is a fully-managed network file system that provides high-performance, scalable Network File System (NFS) shares to Google Cloud workloads. It allows you to mount POSIX-compliant file storage from Compute Engine VMs, GKE clusters and other services without having to provision or manage the underlying storage infrastructure yourself. Each instance resides in a specific region and VPC network, exposes one or more IP addresses, and can be encrypted with either Google-managed or customer-managed keys. +For full details, refer to the official documentation: https://cloud.google.com/filestore/docs. + +**Terrafrom Mappings:** + +- `google_filestore_instance.id` + +## Supported Methods + +- `GET`: Get a gcp-file-instance by its "locations|instances" +- ~~`LIST`~~ +- `SEARCH`: Search for Filestore instances in a location. Use the location string or the full resource name supported for terraform mappings. + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Filestore instance can be configured to use a customer-managed encryption key (CMEK) stored in Cloud KMS. When CMEK is enabled, the instance has a direct dependency on the specified `gcp-cloud-kms-crypto-key`, and loss or revocation of that key will render the file share inaccessible. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every Filestore instance is attached to a single VPC network and is reachable through an internal IP address range that you specify. This link represents the network in which the instance’s NFS endpoints are published and through which client traffic must flow. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md new file mode 100644 index 00000000..0867be52 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md @@ -0,0 +1,13 @@ +--- +title: GCP Iam Role +sidebar_label: gcp-iam-role +--- + +Google Cloud Identity and Access Management (IAM) roles are collections of granular permissions that you grant to principals—such as users, groups or service accounts—so they can interact with Google Cloud resources. Roles come in three varieties (basic, predefined and custom) and are the chief mechanism for enforcing the principle of least privilege across your estate. Overmind represents each IAM role as an individual resource, enabling you to surface the blast-radius of creating, modifying or deleting a role before you commit the change. +For further details, refer to the official Google Cloud documentation: https://cloud.google.com/iam/docs/understanding-roles + +## Supported Methods + +- `GET`: Get a gcp-iam-role by its "name" +- `LIST`: List all gcp-iam-role +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account-key.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account-key.md new file mode 100644 index 00000000..79313f2f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account-key.md @@ -0,0 +1,23 @@ +--- +title: GCP Iam Service Account Key +sidebar_label: gcp-iam-service-account-key +--- + +A GCP IAM Service Account Key is a cryptographic key-pair that allows code or users outside Google Cloud to authenticate as a specific service account. Each key consists of a public key stored by Google and a private key material that can be downloaded once and should be stored securely. Because anyone in possession of the private key can act with all the permissions of the associated service account, these keys are highly sensitive and should be rotated or disabled when no longer required. +For full details, see the official documentation: https://cloud.google.com/iam/docs/creating-managing-service-account-keys + +**Terrafrom Mappings:** + +- `google_service_account_key.id` + +## Supported Methods + +- `GET`: Get GCP Iam Service Account Key by "gcp-iam-service-account-email or unique_id|gcp-iam-service-account-key-name" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Iam Service Account Key by "gcp-iam-service-account-email or unique_id" + +## Possible Links + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Service Account Key is attached to exactly one Service Account; this link allows you to trace which principal will be able to use the key and to evaluate the permissions that could be exercised if the key were compromised. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account.md new file mode 100644 index 00000000..92c34a8d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account.md @@ -0,0 +1,27 @@ +--- +title: GCP Iam Service Account +sidebar_label: gcp-iam-service-account +--- + +A GCP IAM Service Account is a non-human identity that represents a workload such as a VM, Cloud Function or CI/CD pipeline. It can be granted IAM roles and used to obtain access tokens for calling Google Cloud APIs, allowing software to authenticate securely without relying on end-user credentials. Each service account lives inside a single project (or, less commonly, an organisation or folder) and can be equipped with one or more private keys for external use. See the official documentation for further details: [Google Cloud – Service Accounts](https://cloud.google.com/iam/docs/service-accounts). + +**Terrafrom Mappings:** + +- `google_service_account.email` +- `google_service_account.unique_id` + +## Supported Methods + +- `GET`: Get GCP Iam Service Account by "gcp-iam-service-account-email or unique_id" +- `LIST`: List all GCP Iam Service Account items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +Every service account is created within exactly one Cloud Resource Manager project. Overmind links the service account to its parent project so that you can trace inheritance of IAM policies and understand the blast radius of changes to either resource. + +### [`gcp-iam-service-account-key`](/sources/gcp/Types/gcp-iam-service-account-key) + +A service account may have multiple keys (managed by Google or user-managed). These keys allow external systems to impersonate the service account. Overmind enumerates and links all keys associated with a service account, helping you identify stale or over-privileged credentials. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md new file mode 100644 index 00000000..9c101db9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md @@ -0,0 +1,23 @@ +--- +title: GCP Logging Bucket +sidebar_label: gcp-logging-bucket +--- + +A GCP Logging Bucket is a regional or multi-regional storage container within Cloud Logging that holds log entries for long-term retention, analysis and export. Buckets allow you to isolate logs by project, folder or organisation, set individual retention periods, and apply fine-grained IAM policies. They can be configured for customer-managed encryption and for log routing between projects or across the organisation. +For full details see the Google Cloud documentation: https://cloud.google.com/logging/docs/storage#buckets + +## Supported Methods + +- `GET`: Get a gcp-logging-bucket by its "locations|buckets" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-logging-bucket by its "locations" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A logging bucket can be encrypted with a customer-managed encryption key (CMEK). When CMEK is enabled, the bucket stores the full resource name of the Cloud KMS crypto key that protects the log data, creating a dependency on that `gcp-cloud-kms-crypto-key` resource. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Writing, reading and routing logs rely on service accounts such as the Log Router and Google-managed writer accounts. These accounts appear in the bucket’s IAM policy and permissions, so the bucket is linked to the corresponding `gcp-iam-service-account` resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md new file mode 100644 index 00000000..e5e10117 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md @@ -0,0 +1,26 @@ +--- +title: GCP Logging Link +sidebar_label: gcp-logging-link +--- + +A GCP Logging Link is a Cloud Logging resource that connects a Log Bucket to an external analytics destination, currently a BigQuery dataset. Once the link is created, every entry that is written to the bucket is replicated to the linked BigQuery dataset in near real time, letting you query your logs with standard BigQuery SQL without having to configure or manage a separate Log Router sink. +Logging Links are created under the path +`projects/{project}/locations/{location}/buckets/{bucket}/links/{link}` and inherit the life-cycle and IAM policies of their parent bucket. They are regional, can optionally back-fill historical log data at creation time, and can be updated or deleted independently of the bucket or dataset. + +For more information see the official documentation: https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.locations.buckets.links + +## Supported Methods + +- `GET`: Get a gcp-logging-link by its "locations|buckets|links" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-logging-link by its "locations|buckets" + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +A Logging Link points to the BigQuery dataset that serves as the analytics destination. The linked `gcp-big-query-dataset` receives a continuous copy of the logs contained in the parent bucket. + +### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) + +Every Logging Link is defined inside a specific `gcp-logging-bucket`. The bucket is the source of the log entries that are streamed to the linked BigQuery dataset. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-saved-query.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-saved-query.md new file mode 100644 index 00000000..0502885d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-saved-query.md @@ -0,0 +1,13 @@ +--- +title: GCP Logging Saved Query +sidebar_label: gcp-logging-saved-query +--- + +A GCP Logging Saved Query is a reusable, shareable filter definition for Google Cloud Logging (Logs Explorer). It stores the log filter expression, as well as optional display preferences and metadata, so that complex queries can be rerun or shared without having to rewrite the filter each time. Saved queries can be created at the project, folder, billing-account or organisation level and are particularly useful for operational run-books, incident response and dashboards. +Official documentation: https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.locations.savedQueries + +## Supported Methods + +- `GET`: Get a gcp-logging-saved-query by its "locations|savedQueries" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-logging-saved-query by its "locations" diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md new file mode 100644 index 00000000..b1e20cc0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md @@ -0,0 +1,31 @@ +--- +title: GCP Logging Sink +sidebar_label: gcp-logging-sink +--- + +A GCP Logging Sink is an export rule within Google Cloud Logging that continuously routes selected log entries to a destination such as BigQuery, Cloud Storage, Pub/Sub or another Logging bucket. Sinks allow you to retain logs for longer, perform analytics, or trigger near-real-time workflows outside Cloud Logging. Each sink is defined by three core elements: a filter that selects which log entries to export, a destination, and an IAM service account that is granted permission to write to that destination. +For full details see the official documentation: https://cloud.google.com/logging/docs/export/configure_export + +## Supported Methods + +- `GET`: Get GCP Logging Sink by "gcp-logging-sink-name" +- `LIST`: List all GCP Logging Sink items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +If the sink’s destination is set to a BigQuery dataset, Overmind will create a link from the sink to that `gcp-big-query-dataset` resource because the sink writes log rows directly into the dataset’s `_TABLE_SUFFIX` sharded tables. + +### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) + +A sink can either originate from a Logging bucket (when the sink is scoped to that bucket) or target a Logging bucket in another project or billing account. Overmind therefore links the sink to the relevant `gcp-logging-bucket` to show where logs are pulled from or pushed to. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +When a sink exports logs to Pub/Sub, it references a specific topic. Overmind links the sink to the corresponding `gcp-pub-sub-topic` so that users can trace event-driven pipelines or alerting mechanisms that rely on those published log messages. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +If the sink is configured to deliver logs to Cloud Storage, the destination bucket appears as a linked `gcp-storage-bucket`. This highlights where log files are archived and the IAM relationship required for the sink’s writer identity to upload objects. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-alert-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-alert-policy.md new file mode 100644 index 00000000..3521f688 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-alert-policy.md @@ -0,0 +1,23 @@ +--- +title: GCP Monitoring Alert Policy +sidebar_label: gcp-monitoring-alert-policy +--- + +A GCP Monitoring Alert Policy defines the conditions under which Google Cloud Monitoring should raise an alert and the actions that should be taken when those conditions are met. It lets you specify metrics to watch, threshold values, duration, notification channels, documentation for responders, and incident autoclose behaviour. Alert policies are a core part of Google Cloud’s observability suite, helping operations teams detect and respond to issues before they affect end-users. +For full details, see the official documentation: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.alertPolicies#AlertPolicy + +**Terrafrom Mappings:** + +- `google_monitoring_alert_policy.id` + +## Supported Methods + +- `GET`: Get a gcp-monitoring-alert-policy by its "name" +- `LIST`: List all gcp-monitoring-alert-policy +- `SEARCH`: Search by full resource name: projects/[project]/alertPolicies/[alert_policy_id] (used for terraform mapping). + +## Possible Links + +### [`gcp-monitoring-notification-channel`](/sources/gcp/Types/gcp-monitoring-notification-channel) + +An alert policy can reference one or more Notification Channels. These channels determine where alerts are delivered (e-mail, SMS, Pub/Sub, PagerDuty, etc.). Overmind therefore creates a link from each gcp-monitoring-alert-policy to the gcp-monitoring-notification-channel resources it targets, allowing you to understand which channels will be invoked when a policy fires. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-custom-dashboard.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-custom-dashboard.md new file mode 100644 index 00000000..2e1aaa0f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-custom-dashboard.md @@ -0,0 +1,17 @@ +--- +title: GCP Monitoring Custom Dashboard +sidebar_label: gcp-monitoring-custom-dashboard +--- + +A Google Cloud Monitoring Custom Dashboard is a user-defined workspace in which you can visualise metrics, logs-based metrics and alerting information collected from your Google Cloud resources and external services. By assembling charts, heatmaps, and scorecards that matter to your organisation, a custom dashboard lets you observe the real-time health and historical behaviour of your workloads, share operational insights with your team, and troubleshoot incidents more quickly. Dashboards are created and managed through the Cloud Monitoring API, the Google Cloud console, or declaratively via Terraform. +Official documentation: https://cloud.google.com/monitoring/api/ref_v3/rest/v1/projects.dashboards#Dashboard + +**Terrafrom Mappings:** + +- `google_monitoring_dashboard.id` + +## Supported Methods + +- `GET`: Get a gcp-monitoring-custom-dashboard by its "name" +- `LIST`: List all gcp-monitoring-custom-dashboard +- `SEARCH`: Search for custom dashboards by their ID in the form of "projects/[project_id]/dashboards/[dashboard_id]". This is supported for terraform mappings. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-notification-channel.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-notification-channel.md new file mode 100644 index 00000000..f29eba8b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-notification-channel.md @@ -0,0 +1,17 @@ +--- +title: GCP Monitoring Notification Channel +sidebar_label: gcp-monitoring-notification-channel +--- + +A Google Cloud Monitoring Notification Channel is a resource that specifies where and how alerting notifications are delivered from Cloud Monitoring. Channels can point to many target types – e-mail, SMS, mobile push, Slack, PagerDuty, Pub/Sub, webhooks and more – and each channel stores the parameters (addresses, tokens, templates, etc.) required to reach that destination. Alerting policies reference one or more notification channels so that, when a policy is triggered, Cloud Monitoring automatically sends the message to the configured recipients. +For a full description of the resource and its schema, see the official Google Cloud documentation: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.notificationChannels. + +**Terrafrom Mappings:** + +- `google_monitoring_notification_channel.name` + +## Supported Methods + +- `GET`: Get a gcp-monitoring-notification-channel by its "name" +- `LIST`: List all gcp-monitoring-notification-channel +- `SEARCH`: Search by full resource name: projects/[project]/notificationChannels/[notificationChannel] (used for terraform mapping). diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md new file mode 100644 index 00000000..c9c6788c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md @@ -0,0 +1,17 @@ +--- +title: GCP Orgpolicy Policy +sidebar_label: gcp-orgpolicy-policy +--- + +An Organisation Policy in Google Cloud Platform (GCP) lets administrators enforce or relax specific constraints on GCP resources across the organisation, folder, or project hierarchy. Each policy represents the chosen configuration for a single constraint (for example, restricting service account key creation or limiting the set of permitted VM regions) on a single resource node. By querying an Org Policy, Overmind can reveal whether pending changes will violate existing security or governance rules before deployment. +Official documentation: https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints + +**Terrafrom Mappings:** + +- `google_org_policy_policy.name` + +## Supported Methods + +- `GET`: Get a gcp-orgpolicy-policy by its "name" +- `LIST`: List all gcp-orgpolicy-policy +- `SEARCH`: Search with the full policy name: projects/[project]/policies/[constraint] (used for terraform mapping). diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-subscription.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-subscription.md new file mode 100644 index 00000000..16c2256e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-subscription.md @@ -0,0 +1,35 @@ +--- +title: GCP Pub Sub Subscription +sidebar_label: gcp-pub-sub-subscription +--- + +A Google Cloud Pub/Sub subscription represents a named endpoint that receives messages that are published to a specific Pub/Sub topic. Subscribers pull (or are pushed) messages from the subscription, acknowledge them, and thereby remove them from the backlog. Subscriptions can be configured for pull or push delivery, control message-retention, enforce acknowledgement deadlines, use filtering, dead-letter topics or BigQuery/Cloud Storage sinks. +For full details see the official documentation: https://cloud.google.com/pubsub/docs/subscriber + +**Terrafrom Mappings:** + +- `google_pubsub_subscription.name` + +## Supported Methods + +- `GET`: Get a gcp-pub-sub-subscription by its "name" +- `LIST`: List all gcp-pub-sub-subscription +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +A subscription can be of type “BigQuery subscription”, in which case Pub/Sub automatically streams all received messages into the linked BigQuery table. Overmind therefore links the subscription to the destination `gcp-big-query-table` so that you can see where your data will land. + +### [`gcp-pub-sub-subscription`](/sources/gcp/Types/gcp-pub-sub-subscription) + +Multiple subscriptions may exist on the same topic or share common dead-letter topics and filters. Overmind links related subscriptions together so you can understand fan-out patterns or duplicated consumption paths for the same data. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +Every subscription is attached to exactly one topic, from which it receives messages. This parent–child relationship is surfaced by Overmind via a direct link to the source `gcp-pub-sub-topic`. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Storage buckets can emit object-change notifications to Pub/Sub topics. Subscriptions that listen to such topics are therefore operationally coupled to the originating bucket. Overmind links the subscription to the relevant `gcp-storage-bucket` so you can trace the flow of change events from storage to message consumers. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-topic.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-topic.md new file mode 100644 index 00000000..92968f0f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-topic.md @@ -0,0 +1,26 @@ +--- +title: GCP Pub Sub Topic +sidebar_label: gcp-pub-sub-topic +--- + +A Google Cloud Pub/Sub Topic is a named message stream into which publishers send messages and from which subscribers receive them. Topics act as the core distribution point in the Pub/Sub service, decoupling producers and consumers and enabling asynchronous, scalable communication between systems. For full details see the official documentation: https://docs.cloud.google.com/pubsub/docs/create-topic. + +**Terrafrom Mappings:** + +- `google_pubsub_topic.name` + +## Supported Methods + +- `GET`: Get a gcp-pub-sub-topic by its "name" +- `LIST`: List all gcp-pub-sub-topic +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Pub/Sub Topic can be encrypted with a customer-managed Cloud KMS key. When such a key is specified, the topic will hold a reference to the corresponding `gcp-cloud-kms-crypto-key`, and Overmind will surface this dependency so you can assess the impact of key rotation or removal. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Storage buckets can be configured to send event notifications to a Pub/Sub Topic (for example, when objects are created or deleted). Overmind links the bucket to the topic so you can understand which storage resources rely on the topic and evaluate the blast radius of changes to either side. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md new file mode 100644 index 00000000..fe0ec384 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md @@ -0,0 +1,31 @@ +--- +title: GCP Redis Instance +sidebar_label: gcp-redis-instance +--- + +Cloud Memorystore for Redis provides a fully managed, in-memory, open-source Redis service on Google Cloud. It is commonly used for low-latency caching, session management, real-time analytics and message brokering. When you create an instance Google handles provisioning, patching, monitoring, fail-over and, if requested, TLS encryption and customer-managed encryption keys (CMEK). +More information can be found in the official documentation: https://cloud.google.com/memorystore/docs/redis + +**Terrafrom Mappings:** + +- `google_redis_instance.id` + +## Supported Methods + +- `GET`: Get a gcp-redis-instance by its "locations|instances" +- ~~`LIST`~~ +- `SEARCH`: Search Redis instances in a location. Use the format "location" or "projects/[project_id]/locations/[location]/instances/[instance_name]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If CMEK is enabled, the Redis instance is encrypted at rest using a Cloud KMS CryptoKey. Overmind links the instance to the crypto key so you can trace data-at-rest encryption dependencies and evaluate key rotation or IAM policies. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Each Redis instance is created inside a specific VPC network and subnet. Linking to the compute network allows you to understand network reachability, firewall rules and peering arrangements that could affect the instance. + +### [`gcp-compute-ssl-certificate`](/sources/gcp/Types/gcp-compute-ssl-certificate) + +When TLS is enabled, Redis serves Google-managed certificates under the hood. Overmind associates these certificates (represented as Compute SSL Certificate resources) so that certificate expiry and chain of trust can be audited alongside the Redis service. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md new file mode 100644 index 00000000..5368eede --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md @@ -0,0 +1,46 @@ +--- +title: GCP Run Revision +sidebar_label: gcp-run-revision +--- + +A Cloud Run Revision represents an immutable snapshot of the code and configuration that Cloud Run executes. Every time you deploy a new container image or change the runtime configuration of a Cloud Run Service, a new Revision is created and given a unique name. The Revision stores details such as the container image reference, environment variables, scaling limits, traffic settings, networking options and the service account under which the workload runs. Official documentation: https://docs.cloud.google.com/run/docs/managing/revisions + +## Supported Methods + +- `GET`: Get a gcp-run-revision by its "locations|services|revisions" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-run-revision by its "locations|services" + +## Possible Links + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +The Revision’s `container.image` field points to a Docker image that is normally stored in Artifact Registry (or the older Container Registry). Overmind therefore links the Revision to the exact image digest it deploys, so you can see what code is really running. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the Revision mounts secrets or other resources that are encrypted with Cloud KMS, those crypto-keys are surfaced as links. This helps you understand which keys would be required to decrypt data at runtime. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When a Revision is configured with a Serverless VPC Connector or egress settings that reference a particular VPC network, the corresponding `compute.network` is linked. This reveals the network perimeter through which outbound traffic may flow. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Similarly, a Revision may target a specific sub-network (for example `vpcAccess.connectorSubnetwork`). Overmind links the Revision to that `compute.subnetwork` so you can trace which CIDR ranges and routes apply. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Each Revision runs with an IAM service account specified in its `serviceAccountName` field. Linking to the service account lets you inspect the permissions that the workload inherits. + +### [`gcp-run-service`](/sources/gcp/Types/gcp-run-service) + +A Revision belongs to exactly one Cloud Run Service. The link to the parent Service shows the traffic allocation, routing configuration and other higher-level settings that govern how the Revision is invoked. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +If the Revision’s metadata includes Cloud SQL connection strings (via the `cloudSqlInstances` setting), Overmind links to the referenced Cloud SQL instances, making database dependencies explicit. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Revisions can mount Cloud Storage buckets using Cloud Storage FUSE volumes or reference buckets through environment variables. When such configuration is detected, the corresponding buckets are linked so you can assess data-at-rest exposure. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md new file mode 100644 index 00000000..deff181d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md @@ -0,0 +1,54 @@ +--- +title: GCP Run Service +sidebar_label: gcp-run-service +--- + +Cloud Run Service is a fully-managed container execution environment that lets you run stateless HTTP containers on demand within Google Cloud. A Service represents the top-level Cloud Run resource, providing a stable URL, traffic splitting, configuration, and revision management for your containerised workload. For full details see the Google Cloud documentation: https://cloud.google.com/run/docs/reference/rest/v2/projects.locations.services + +**Terrafrom Mappings:** + +- `google_cloud_run_v2_service.id` + +## Supported Methods + +- `GET`: Get a gcp-run-service by its "locations|services" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-run-service by its "locations" + +## Possible Links + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +A Cloud Run Service pulls its container image from Artifact Registry (or Container Registry). The linked `gcp-artifact-registry-docker-image` represents the specific image digest or tag referenced in the Service spec. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the Service’s container image or any attached Secret Manager secret is encrypted with a customer-managed encryption key (CMEK), the Cloud Run Service will be linked to the corresponding `gcp-cloud-kms-crypto-key`. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When a Cloud Run Service is configured with a Serverless VPC Access connector, it attaches to a VPC network to reach private resources. That network is represented here as a `gcp-compute-network`. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +The Serverless VPC Access connector also lives on a particular subnetwork. The Cloud Run Service therefore relates to the `gcp-compute-subnetwork` used for outbound traffic. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Cloud Run Service executes with an identity (the “service account” set in the Service’s `executionEnvironment` or `serviceAccount`). This runtime identity is captured as a link to `gcp-iam-service-account`. + +### [`gcp-run-revision`](/sources/gcp/Types/gcp-run-revision) + +Each deployment of a Cloud Run Service creates an immutable Revision. The Service maintains traffic routing rules among its Revisions, so it links to one or more `gcp-run-revision` resources. + +### [`gcp-secret-manager-secret`](/sources/gcp/Types/gcp-secret-manager-secret) + +Environment variables or mounted volumes can reference secrets stored in Secret Manager. Any such secret referenced by the Service or its Revisions appears as a `gcp-secret-manager-secret` link. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +If the Service includes a Cloud SQL connection string (via the `cloudsql-instances` annotation), Overmind records a relationship to the corresponding `gcp-sql-admin-instance`. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Run Services may interact with Cloud Storage—for example, by having a URL environment variable or event trigger configuration. Where such a bucket name is detected in the Service configuration, it is linked here as `gcp-storage-bucket`. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-secret-manager-secret.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-secret-manager-secret.md new file mode 100644 index 00000000..be765812 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-secret-manager-secret.md @@ -0,0 +1,26 @@ +--- +title: GCP Secret Manager Secret +sidebar_label: gcp-secret-manager-secret +--- + +A Google Cloud Secret Manager Secret is the logical container for sensitive data such as API keys, passwords and certificates stored in Secret Manager. The secret resource defines metadata and access-control policies, while one or more numbered “versions” hold the actual payload, enabling safe rotation and roll-back. Secrets are encrypted at rest with Google-managed keys by default, or with a user-supplied Cloud KMS key, and access is governed through IAM. For further information see the official documentation: https://cloud.google.com/secret-manager/docs + +**Terrafrom Mappings:** + +- `google_secret_manager_secret.secret_id` + +## Supported Methods + +- `GET`: Get a gcp-secret-manager-secret by its "name" +- `LIST`: List all gcp-secret-manager-secret +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If a customer-managed encryption key (CMEK) has been configured for this secret, the secret’s `kms_key_name` field will reference a Cloud KMS Crypto Key. Overmind surfaces that link so that you can trace how the secret is encrypted and assess key-management risks. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +Secret Manager can be set to publish notifications (e.g. when a new secret version is added or destroyed) to a Pub/Sub topic. When such a notification configuration exists, the secret will link to the relevant Pub/Sub topic, allowing you to review who can subscribe to, or forward, these events. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-security-center-management-security-center-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-security-center-management-security-center-service.md new file mode 100644 index 00000000..32c701bf --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-security-center-management-security-center-service.md @@ -0,0 +1,13 @@ +--- +title: GCP Security Center Management Security Center Service +sidebar_label: gcp-security-center-management-security-center-service +--- + +A Security Center Service resource represents the activation and configuration of Google Cloud Security Command Center (SCC) for a particular location (for example `europe-west2`) within a project, folder, or organisation. It records whether SCC is enabled, the current service tier (Standard, Premium, or Enterprise), and other operational metadata such as activation time and billing status. Administrators use this resource to programme-matically enable or disable SCC, upgrade or downgrade the service tier, and verify the health of the service across all regions. +Official documentation: https://cloud.google.com/security-command-center/docs/reference/security-center-management/rest/v1/folders.locations.securityCenterServices#SecurityCenterService + +## Supported Methods + +- `GET`: Get a gcp-security-center-management-security-center-service by its "locations|securityCenterServices" +- ~~`LIST`~~ +- `SEARCH`: Search Security Center services in a location. Use the format "location". diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-directory-endpoint.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-directory-endpoint.md new file mode 100644 index 00000000..bb6b4f89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-directory-endpoint.md @@ -0,0 +1,23 @@ +--- +title: GCP Service Directory Endpoint +sidebar_label: gcp-service-directory-endpoint +--- + +A Service Directory Endpoint represents a concrete network destination that backs a Service Directory Service inside Google Cloud. Each endpoint records the IP address, port and (optionally) metadata that client workloads use to discover and call the service. Endpoints are created inside a hierarchy of **Project → Location → Namespace → Service → Endpoint** and are resolved at run-time through Service Directory’s DNS or HTTP APIs, allowing producers to register instances and consumers to discover them without hard-coding addresses. +Official documentation: https://cloud.google.com/service-directory/docs/reference/rest/v1/projects.locations.namespaces.services.endpoints + +**Terrafrom Mappings:** + +- `google_service_directory_endpoint.id` + +## Supported Methods + +- `GET`: Get a gcp-service-directory-endpoint by its "locations|namespaces|services|endpoints" +- ~~`LIST`~~ +- `SEARCH`: Search for endpoints by "location|namespace_id|service_id" or "projects/[project_id]/locations/[location]/namespaces/[namespace_id]/services/[service_id]/endpoints/[endpoint_id]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Each endpoint is associated with a specific VPC network; the `network` field determines from which network the endpoint can be reached and which clients can resolve it. When Overmind discovers a Service Directory Endpoint, it links the item to the corresponding gcp-compute-network so you can trace service discovery issues back to network configuration or segmentation problems. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-usage-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-usage-service.md new file mode 100644 index 00000000..eb94c112 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-usage-service.md @@ -0,0 +1,20 @@ +--- +title: GCP Service Usage Service +sidebar_label: gcp-service-usage-service +--- + +Represents an individual Google Cloud API or service (for example, `pubsub.googleapis.com`, `compute.googleapis.com`) that can be enabled or disabled within a project or folder via the Service Usage API. +It holds metadata such as the service’s name, state (ENABLED, DISABLED, etc.), configuration and any consumer-specific settings. Managing this resource controls whether dependent resources in the project are allowed to operate. +Official documentation: https://cloud.google.com/service-usage/docs/overview + +## Supported Methods + +- `GET`: Get a gcp-service-usage-service by its "name" +- `LIST`: List all gcp-service-usage-service +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +A Pub/Sub topic can only exist and function if the `pubsub.googleapis.com` service is ENABLED in the same project. Overmind links a `gcp-service-usage-service` whose name is `pubsub.googleapis.com` to all `gcp-pub-sub-topic` resources in that project so that you can assess the blast radius of disabling the API. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md new file mode 100644 index 00000000..7206a904 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md @@ -0,0 +1,28 @@ +--- +title: GCP Spanner Database +sidebar_label: gcp-spanner-database +--- + +Google Cloud Spanner is Google Cloud’s fully-managed, horizontally-scalable, relational database service. +A Spanner **database** is the logical container that holds your tables, schema objects and data inside a Spanner instance. Each database inherits the instance’s compute and storage configuration and can be encrypted either with Google-managed keys or with a customer-managed key (CMEK). +For an overview of the service see the official documentation: https://cloud.google.com/spanner/docs/overview + +**Terrafrom Mappings:** + +- `google_spanner_database.name` + +## Supported Methods + +- `GET`: Get a gcp-spanner-database by its "instances|databases" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-spanner-database by its "instances" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Spanner database can be encrypted with a customer-managed encryption key (CMEK) stored in Cloud KMS. When CMEK is enabled, the database resource is linked to the specific `gcp-cloud-kms-crypto-key` that provides its encryption. + +### [`gcp-spanner-instance`](/sources/gcp/Types/gcp-spanner-instance) + +Every Spanner database lives inside a Spanner instance. The database inherits performance characteristics and regional configuration from its parent `gcp-spanner-instance`, making this a direct parent–child relationship. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md new file mode 100644 index 00000000..b3820f65 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md @@ -0,0 +1,23 @@ +--- +title: GCP Spanner Instance +sidebar_label: gcp-spanner-instance +--- + +A **Cloud Spanner instance** is the top-level container for Cloud Spanner resources in Google Cloud. It specifies the geographic placement of the underlying nodes, the amount of compute capacity allocated (measured in processing units), and the instance’s name and labels. All Cloud Spanner databases and their data live inside an instance, and the instance’s configuration determines their availability and latency characteristics. +For full details see the Google Cloud documentation: https://cloud.google.com/spanner/docs/instances + +**Terrafrom Mappings:** + +- `google_spanner_instance.name` + +## Supported Methods + +- `GET`: Get a gcp-spanner-instance by its "name" +- `LIST`: List all gcp-spanner-instance +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-spanner-database`](/sources/gcp/Types/gcp-spanner-database) + +A Cloud Spanner instance can host one or more Cloud Spanner databases. Each `gcp-spanner-database` discovered by Overmind will therefore be linked to the `gcp-spanner-instance` that contains it, allowing you to see which databases would be affected by changes to, or deletion of, the parent instance. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup-run.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup-run.md new file mode 100644 index 00000000..7192c072 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup-run.md @@ -0,0 +1,22 @@ +--- +title: GCP Sql Admin Backup Run +sidebar_label: gcp-sql-admin-backup-run +--- + +A GCP SQL Admin Backup Run represents an individual on-demand or automatically-scheduled backup created for a Cloud SQL instance. Each backup run records metadata such as its status, start and end times, location, encryption information and size. Backup runs are managed through the Cloud SQL Admin API and can be listed, retrieved or deleted by project administrators. For full details see Google’s documentation: https://cloud.google.com/sql/docs/mysql/admin-api/rest/v1/backupRuns + +## Supported Methods + +- `GET`: Get a gcp-sql-admin-backup-run by its "instances|backupRuns" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-sql-admin-backup-run by its "instances" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If a Cloud SQL instance is configured with customer-managed encryption keys (CMEK), the backup run is encrypted with the specified KMS CryptoKey. The backup run therefore references the CryptoKey used for encryption. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +Every backup run belongs to exactly one Cloud SQL instance; the instance is the parent resource under which the backup run is created. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup.md new file mode 100644 index 00000000..3c686d99 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup.md @@ -0,0 +1,28 @@ +--- +title: GCP Sql Admin Backup +sidebar_label: gcp-sql-admin-backup +--- + +A **GCP Sql Admin Backup** represents the backup configuration that protects a Cloud SQL instance. +The object contains the settings that determine when and how Google Cloud takes automatic or on-demand snapshots of the instance, including the backup window, retention period, and (when Customer-Managed Encryption Keys are used) the CryptoKey that encrypts the resulting files. +For a detailed description of Cloud SQL backups see the official documentation: https://cloud.google.com/sql/docs/mysql/backup-recovery/backups. + +## Supported Methods + +- `GET`: Get a gcp-sql-admin-backup by its "name" +- `LIST`: List all gcp-sql-admin-backup +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the backup is encrypted with a Customer-Managed Encryption Key (CMEK), Overmind links the backup to the `gcp-cloud-kms-crypto-key` that holds the key material. Analysing this relationship lets you verify that the key exists, is in the correct state, and has the appropriate IAM policy. + +### [`gcp-sql-admin-backup-run`](/sources/gcp/Types/gcp-sql-admin-backup-run) + +Every time the backup configuration is executed it produces a Backup Run. This link connects the configuration to those individual `gcp-sql-admin-backup-run` objects, allowing you to trace whether recent runs succeeded and to inspect metadata such as the size and status of each run. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +The backup configuration belongs to a specific Cloud SQL instance. This link points from the backup resource to the parent `gcp-sql-admin-instance`, helping you understand which database workload the backup protects and enabling dependency traversal from the instance to its safety mechanisms. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-instance.md new file mode 100644 index 00000000..86a0993c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-instance.md @@ -0,0 +1,42 @@ +--- +title: GCP Sql Admin Instance +sidebar_label: gcp-sql-admin-instance +--- + +A GCP SQL Admin Instance represents a managed Cloud SQL database instance in Google Cloud Platform. It encapsulates the configuration of the database engine (MySQL, PostgreSQL or SQL Server), machine tier, storage, high-availability settings, networking and encryption options. The resource is managed through the Cloud SQL Admin API, which is documented here: https://cloud.google.com/sql/docs/mysql/admin-api/. Creating or modifying an instance via Terraform, the Cloud Console or gcloud ultimately results in API calls against this object. + +**Terrafrom Mappings:** + +- `google_sql_database_instance.name` + +## Supported Methods + +- `GET`: Get a gcp-sql-admin-instance by its "name" +- `LIST`: List all gcp-sql-admin-instance +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If Customer-Managed Encryption Keys (CMEK) are enabled for the instance, the instance is encrypted with a specific Cloud KMS Crypto Key. Overmind links the instance to the `gcp-cloud-kms-crypto-key` that provides its disk-level encryption key. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When an instance is configured for private IP or has authorised networks for public IP access, it attaches to one or more VPC networks. Overmind therefore links the instance to the `gcp-compute-network` resources that define those VPCs. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Cloud SQL automatically creates or uses service accounts to perform backups, replication and other administrative tasks. The instance is linked to the `gcp-iam-service-account` identities that act on its behalf, allowing you to trace permissions and potential privilege escalation paths. + +### [`gcp-sql-admin-backup-run`](/sources/gcp/Types/gcp-sql-admin-backup-run) + +Each automated or on-demand backup of an instance is represented by a Backup Run resource. Overmind links every `gcp-sql-admin-backup-run` to the parent instance so you can see the full backup history and retention compliance. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +Instances may reference other instances when configured for read replicas, high-availability failover or cloning. Overmind links an instance to any peer `gcp-sql-admin-instance` that serves as its primary, replica or clone source/target. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud SQL supports import/export of SQL dump files and automatic log exports to Cloud Storage. The instance is linked to any `gcp-storage-bucket` that it reads from or writes to during these operations, revealing data-exfiltration or retention risks. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md new file mode 100644 index 00000000..a4b4edde --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md @@ -0,0 +1,30 @@ +--- +title: GCP Storage Bucket +sidebar_label: gcp-storage-bucket +--- + +A GCP Storage Bucket is a logical container in Google Cloud Storage that holds your objects (blobs). Buckets provide globally-unique namespaces, configurable lifecycle policies, access controls, versioning, and encryption options, allowing organisations to store and serve unstructured data such as backups, media files, or static web assets. See the official documentation for full details: https://cloud.google.com/storage/docs/key-terms#buckets + +**Terrafrom Mappings:** + +- `google_storage_bucket.name` + +## Supported Methods + +- `GET`: Get a gcp-storage-bucket by its "name" +- `LIST`: List all gcp-storage-bucket +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Instances and other compute resources that run inside a VPC network often read from or write to a Storage Bucket. Additionally, when Private Google Access or VPC Service Controls are enabled, the bucket’s accessibility is governed by the associated compute network, creating a security dependency between the two resources. + +### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) + +Audit logs for a Storage Bucket can be routed into a Cloud Logging bucket, and Logging buckets can export their contents to a Storage Bucket. Either configuration establishes a link whereby changes to the Storage Bucket may affect log retention and compliance. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Storage Bucket can be configured to use Customer-Managed Encryption Keys (CMEK). When this option is enabled, the bucket references a Cloud KMS CryptoKey for data-at-rest encryption, making the bucket’s availability and security reliant on the referenced key’s state and permissions. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-transfer-transfer-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-transfer-transfer-job.md new file mode 100644 index 00000000..01476fc4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-transfer-transfer-job.md @@ -0,0 +1,34 @@ +--- +title: GCP Storage Transfer Transfer Job +sidebar_label: gcp-storage-transfer-transfer-job +--- + +A Storage Transfer Service Job represents a scheduled or on-demand operation that copies data between cloud storage systems or from on-premises sources into Google Cloud Storage. A job defines source and destination locations, transfer options (such as whether to delete objects after transfer), scheduling, and optional notifications. For full details see the official Google documentation: https://cloud.google.com/storage-transfer/docs/overview + +**Terrafrom Mappings:** + +- `google_storage_transfer_job.name` + +## Supported Methods + +- `GET`: Get a gcp-storage-transfer-transfer-job by its "name" +- `LIST`: List all gcp-storage-transfer-transfer-job +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Storage Transfer Service creates and utilises a dedicated service account to read from the source and write to the destination. The transfer job must have the correct IAM roles granted on this service account, making the two resources inherently linked. + +### [`gcp-pub-sub-subscription`](/sources/gcp/Types/gcp-pub-sub-subscription) + +If transfer job notifications are configured, the Storage Transfer Service publishes messages to a Pub/Sub topic. A subscription attached to that topic receives the events, so a job that emits notifications will be related to the downstream subscriptions. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +The transfer job can be configured to send success, failure, or progress notifications to a specific Pub/Sub topic. That topic therefore has a direct relationship with the job. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Buckets are commonly used as both sources and destinations for transfer jobs. Any bucket referenced in the `transferSpec` of a job (either as a source or destination) is linked to that job. diff --git a/docs.overmind.tech/docs/sources/gcp/_category_.json b/docs.overmind.tech/docs/sources/gcp/_category_.json new file mode 100644 index 00000000..80514572 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "GCP 🆕", + "position": 3, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to integrate your Google Cloud Platform" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/configuration.md b/docs.overmind.tech/docs/sources/gcp/configuration.md new file mode 100644 index 00000000..450d3b11 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/configuration.md @@ -0,0 +1,439 @@ +--- +title: GCP Configuration +sidebar_position: 1 +--- + +# GCP Configuration + +## Overview + +Overmind's GCP infrastructure discovery provides comprehensive visibility into your Google Cloud Platform resources through secure, read-only access using Google Cloud's native IAM system. + +Overmind supports two authentication methods: + +1. **Direct Access** (Default) - Grant permissions directly to the Overmind service account +2. **Service Account Impersonation** (Optional) - Create your own service account with permissions, then allow Overmind to impersonate it + +Both methods provide the same functionality and security. Choose the method that fits your organization's security policies. + +### Authentication Methods Comparison + +**Direct Access:** + +- Simplest setup - grant roles directly to Overmind's service account +- Best for quick setup and straightforward security requirements + +**Service Account Impersonation:** + +- Enhanced control - you create and manage your own service account +- Better for organizations requiring all service accounts to be internally managed +- Provides dual identity in audit logs (both Overmind's SA and your SA) +- Learn more: [GCP Service Account Impersonation](https://cloud.google.com/iam/docs/service-account-impersonation) + +### Why Service Account-Based Access? + +Each customer receives a unique Overmind service account with minimal, read-only permissions. All access is logged through Google Cloud's audit system, giving you complete control with no shared credentials. This aligns with [Google Cloud's security best practices](https://cloud.google.com/security/best-practices). + +## Prerequisites + +Before beginning setup, ensure you have: + +- **GCP Resource Access**: Appropriate IAM admin permissions at the organization, folder, or project level to grant IAM roles (and create service accounts for impersonation) +- **Required Tools**: One of the following: + - [Google Cloud CLI (`gcloud`)](https://cloud.google.com/sdk/docs/install) installed and authenticated + - Terraform with the Google Cloud Provider configured +- **Parent Resource**: The parent resource ID where Overmind will discover resources. This can be: + - An organization: `organizations/123456789` + - A folder: `folders/987654321` + - A project: `projects/my-project-id` +- **Regional Scope**: List of GCP regions where your resources are located (mandatory for source configuration) + +### Authentication Setup + +Ensure your local environment is authenticated with Google Cloud: + +```bash +# Authenticate with Google Cloud +gcloud auth login + +# Set your default project (if using a project as parent) +gcloud config set project YOUR_PROJECT_ID + +# Verify authentication +gcloud auth list +``` + +For Terraform users, configure [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials): + +```bash +gcloud auth application-default login +``` + +## Quick Start + +### Step 1: Create Your Overmind GCP Source + +1. Navigate to **Settings** > **Sources** > **Add Source** > **GCP** in the Overmind application +2. Configure your source: + - **Parent ID**: The parent resource to discover from. Format: + - Organization: `organizations/123456789` + - Folder: `folders/987654321` + - Project: `projects/my-project-id` + - **Name**: A descriptive name for this source (optional) + - **Regions**: Select the regions where your resources are located (mandatory) + - **Impersonation** (optional): Toggle on to use service account impersonation + - Enter the email of the service account you'll create (e.g., `overmind-reader@your-project.iam.gserviceaccount.com`) + - Use any unique name for your service account +3. Click **Create Source** + +You'll be redirected to the source details page showing: + +- The Overmind service account email (e.g., `C-xxxxx@ovm-production.iam.gserviceaccount.com`) +- Configuration instructions customized for your setup +- Whether impersonation is enabled + +### Step 2: Grant Permissions + +The source details page provides customized scripts for your setup. These scripts automatically apply IAM permissions at the level you specified (organization, folder, or project). Permissions granted at a parent level are inherited by all child resources. + +Choose your preferred method: + +#### Option A: Cloud Shell (Easiest) + +Click the **"Open in Google Cloud Shell"** button shown on the source details page. This provides you with the scripts and guidance needed to complete the setup. Follow the instructions in Cloud Shell to run the appropriate setup script for your configuration. + +#### Option B: Manual Script + +Copy and run the bash script shown on the source details page. The script automatically detects whether you're using an organization, folder, or project parent and applies the correct `gcloud` commands. The script varies based on whether impersonation is enabled: + +**For Direct Access:** + +- Grants read-only roles directly to the Overmind service account at your specified parent level +- For project-level parents, also creates a custom role for additional permissions + +**For Impersonation:** + +- Grants read-only roles to your service account at your specified parent level (you must create the service account manually first) +- For project-level parents, also creates a custom role for additional permissions +- Grants Overmind's service account permission to impersonate yours (`roles/iam.serviceAccountTokenCreator`) + +#### Option C: Terraform + +Copy the Terraform configuration shown on the source details page and apply it: + +```bash +terraform init +terraform plan +terraform apply +``` + +### Step 3: Verify Source Status + +1. Navigate to **Settings** > **Sources** in the Overmind application +2. Verify your GCP source shows as **Healthy** + +## Required Permissions + +Overmind requires read-only IAM roles for infrastructure discovery. See the [Required GCP Roles Reference](#required-gcp-roles-reference) for the complete list. + +### Permission Flow + +Permissions can be applied at any level of the GCP resource hierarchy and are inherited by child resources: + +**Direct Access:** + +``` +Your GCP Organization/Folder/Project + └─ Overmind Service Account + └─ Granted: Viewer roles (+ custom role for project-level) + └─ Inherited by all child folders and projects +``` + +**Service Account Impersonation:** + +``` +Your GCP Organization/Folder/Project + ├─ Your Service Account + │ └─ Granted: Viewer roles (+ custom role for project-level) + │ └─ Inherited by all child folders and projects + └─ Overmind Service Account + └─ Granted: roles/iam.serviceAccountTokenCreator on Your Service Account +``` + +## Switching Between Authentication Methods + +### Enable Impersonation + +1. Create a service account in your GCP project (if you haven't already) +2. Grant it the required read-only roles and impersonation permission (use the scripts from the source details page - they handle both) +3. Edit your source in Overmind: enable **Impersonation** and enter your service account email +4. (Optional) Remove direct permissions from Overmind's service account + +### Disable Impersonation + +1. Edit your source in Overmind: disable **Impersonation** (this updates the scripts on the source details page) +2. Grant the required read-only roles directly to Overmind's service account (use the updated scripts from the source details page) +3. (Optional) Remove the impersonation permission and delete your service account + +## Validation + +### Verify IAM Permissions + +**Using Google Cloud Console:** + +1. Navigate to [IAM & Admin > IAM](https://console.cloud.google.com/iam-admin/iam) +2. Select your organization, folder, or project +3. Search for the service account (Overmind's or yours, depending on setup) +4. Verify all required roles are listed + +**Using Google Cloud CLI:** + +For direct access at organization level: + +```bash +gcloud organizations get-iam-policy YOUR_ORG_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role)" \ + --filter="bindings.members:serviceAccount:OVERMIND_SA_EMAIL" +``` + +For direct access at folder level: + +```bash +gcloud resource-manager folders get-iam-policy YOUR_FOLDER_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role)" \ + --filter="bindings.members:serviceAccount:OVERMIND_SA_EMAIL" +``` + +For direct access at project level: + +```bash +gcloud projects get-iam-policy YOUR_PROJECT_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role)" \ + --filter="bindings.members:serviceAccount:OVERMIND_SA_EMAIL" +``` + +For impersonation (verify Overmind can impersonate your SA): + +```bash +gcloud iam service-accounts get-iam-policy YOUR_SA_EMAIL \ + --project=YOUR_PROJECT_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role,bindings.members)" \ + --filter="bindings.members:serviceAccount:OVERMIND_SA_EMAIL" +``` + +### Test Source Discovery + +1. Navigate to **Explore** in the Overmind application +2. Run a query: GCP sources are prefixed with `gcp-` + - To list all VMs: `gcp-compute-instance` > `LIST` +3. Verify resources are being discovered + +### Validate Regional Coverage + +Review the **Regions** configuration in your source settings and verify discovered resources match your expected regional distribution. + +## Troubleshooting + +### Common Issues + +**"Insufficient Permissions" Error** + +Verify all required roles are assigned at the appropriate level: + +```bash +# For organization-level access +gcloud organizations get-iam-policy YOUR_ORG_ID \ + --flatten="bindings[].members" \ + --filter="bindings.members:serviceAccount:SA_EMAIL" + +# For folder-level access +gcloud resource-manager folders get-iam-policy YOUR_FOLDER_ID \ + --flatten="bindings[].members" \ + --filter="bindings.members:serviceAccount:SA_EMAIL" + +# For project-level access +gcloud projects get-iam-policy YOUR_PROJECT_ID \ + --flatten="bindings[].members" \ + --filter="bindings.members:serviceAccount:SA_EMAIL" +``` + +Re-run the setup script or check for organization-level policies restricting service account access. + +**No Resources Discovered** + +1. Verify regional configuration matches where your resources exist +2. For project-level parents, check that required GCP APIs are enabled: + ```bash + gcloud services list --enabled --project=YOUR_PROJECT_ID + ``` +3. For organization or folder-level parents, verify that you have the necessary permissions to list projects and that child projects have the required APIs enabled +4. Some resources may require additional permissions at different levels of the hierarchy + +**Service Account Impersonation Fails** + +1. Verify the impersonation permission is granted: + + ```bash + gcloud iam service-accounts get-iam-policy YOUR_SA_EMAIL --project=YOUR_PROJECT_ID + ``` + + You should see Overmind's service account with `roles/iam.serviceAccountTokenCreator`. + +2. Verify your service account exists and isn't disabled: + + ```bash + gcloud iam service-accounts describe YOUR_SA_EMAIL --project=YOUR_PROJECT_ID + ``` + +3. Ensure the service account email in Overmind matches exactly + +4. Wait for propagation: IAM policy changes can take a few minutes to propagate. Wait 2-5 minutes after granting permissions before testing. + +5. Check organization policies: Some organization policies may restrict service account impersonation. + +**Service Account Not Found** + +1. Verify you copied the correct email from the Overmind application +2. Ensure the email format is correct (ends with `.iam.gserviceaccount.com`) +3. For impersonation: verify your service account was created successfully +4. Contact [Overmind support](https://docs.overmind.tech/misc/support) if issues persist + +**Terraform Apply Failures** + +1. Verify authentication: `gcloud auth application-default print-access-token` +2. Ensure your credentials have necessary IAM permissions +3. For impersonation: ensure you have `iam.serviceAccounts.create` permission + +### Getting Help + +If you continue to experience issues, contact [Overmind support](https://docs.overmind.tech/misc/support) with: + +- Your GCP parent resource (organization/folder/project ID) +- The Overmind service account email +- Your service account email (if using impersonation) +- Whether you're using direct access or impersonation +- The parent level you're configuring (organization, folder, or project) +- Specific error messages and screenshots + +## Security Considerations + +### Principle of Least Privilege + +All roles are read-only and do not allow: + +- Resource modification or deletion +- Data access (beyond metadata) +- Configuration changes +- Administrative operations + +### Monitoring and Auditing + +1. Enable [Cloud Audit Logs](https://cloud.google.com/logging/docs/audit) for your project +2. Monitor service account activity in audit logs +3. Configure alerts for unusual behavior + +**Impersonation Audit Benefits:** +With impersonation, audit logs show both Overmind's identity and your service account identity, providing enhanced traceability. + +### Permission Management + +- **Regular Review**: Periodically review granted permissions +- **Revocation**: Remove access anytime: + - **Direct access**: Remove IAM bindings + - **Impersonation**: Remove `serviceAccountTokenCreator` role or disable/delete your service account + +## Required Permissions + +Overmind requires read-only access to discover and map your GCP infrastructure. The setup scripts provided in the Overmind application automatically grant all necessary permissions. + +### What Gets Configured + +**Essential role for resource discovery:** + +- `roles/browser` - Required for listing projects and navigating the resource hierarchy + +**Read-only viewer roles** for GCP services including: + +- Compute Engine, GKE, Cloud Run, Cloud Functions +- Cloud SQL, BigQuery, Spanner, Cloud Storage +- IAM, networking, monitoring, and logging resources +- And other GCP services + +**A custom role** with additional permissions for: + +- BigQuery data transfer configurations +- Spanner database details + +**Project-level-only roles** (only applied when using `projects/` parent): + +- `roles/iam.roleViewer` - View IAM roles +- `roles/iam.serviceAccountViewer` - View service accounts + +> **Note:** Some GCP IAM roles can only be granted at the project level, not at the organization or folder level. When configuring at the organization or folder level, these project-specific roles are automatically excluded. The custom role and project-level IAM roles are only created and assigned when using a project-level parent (e.g., `projects/my-project`). + +**For impersonation** (if enabled): + +- `roles/iam.serviceAccountTokenCreator` - Allows Overmind to impersonate your service account + +All permissions are read-only and do not allow resource modification, deletion, or access to data beyond metadata. + +The complete list of roles is included in the setup scripts shown in your source details page. These scripts are automatically updated as Overmind adds support for new GCP services and adapt based on whether you're configuring at the organization, folder, or project level. + +## Required GCP Roles Reference + +Here are all the predefined GCP roles that Overmind requires, plus the custom role for additional permissions: + +### Predefined Roles + +| Role | Purpose | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `roles/browser` | **Required:** List projects and navigate resource hierarchy [GCP Docs](https://cloud.google.com/iam/docs/understanding-roles#browser) | +| `roles/aiplatform.viewer` | AI Platform resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/aiplatform#aiplatform.viewer) | +| `roles/artifactregistry.reader` | Artifact Registry repository discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/artifactregistry#artifactregistry.reader) | +| `roles/bigquery.metadataViewer` | BigQuery metadata discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/bigquery#bigquery.metadataViewer) | +| `roles/bigquery.user` | BigQuery data transfer discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/bigquery#bigquery.user) | +| `roles/bigtable.viewer` | Cloud Bigtable resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/bigtable#bigtable.viewer) | +| `roles/cloudbuild.builds.viewer` | Cloud Build resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/cloudbuild#cloudbuild.builds.viewer) | +| `roles/cloudfunctions.viewer` | Cloud Functions discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/cloudfunctions#cloudfunctions.viewer) | +| `roles/cloudkms.viewer` | Cloud KMS resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/cloudkms#cloudkms.viewer) | +| `roles/cloudsql.viewer` | Cloud SQL instance discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/cloudsql#cloudsql.viewer) | +| `roles/compute.viewer` | Compute Engine resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/compute#compute.viewer) | +| `roles/container.viewer` | GKE cluster and resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/container#container.viewer) | +| `roles/dataform.viewer` | Dataform resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dataform#dataform.viewer) | +| `roles/dataplex.catalogViewer` | Dataplex catalog resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dataplex#dataplex.catalogViewer) | +| `roles/dataplex.viewer` | Dataplex resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dataplex#dataplex.viewer) | +| `roles/dataproc.viewer` | Dataproc cluster discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dataproc#dataproc.viewer) | +| `roles/dns.reader` | Cloud DNS resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dns#dns.reader) | +| `roles/essentialcontacts.viewer` | Essential Contacts discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/essentialcontacts#essentialcontacts.viewer) | +| `roles/eventarc.viewer` | Eventarc trigger discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/eventarc#eventarc.viewer) | +| `roles/file.viewer` | Cloud Filestore discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/file#file.viewer) | +| `roles/iam.roleViewer` | **Project-level only:** IAM role discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/iam#iam.roleViewer) | +| `roles/iam.serviceAccountViewer` | **Project-level only:** IAM service account discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/iam#iam.serviceAccountViewer) | +| `roles/logging.viewer` | Cloud Logging resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/logging#logging.viewer) | +| `roles/monitoring.viewer` | Cloud Monitoring resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/monitoring#monitoring.viewer) | +| `roles/orgpolicy.policyViewer` | Organization Policy discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/orgpolicy#orgpolicy.policyViewer) | +| `roles/pubsub.viewer` | Pub/Sub resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/pubsub#pubsub.viewer) | +| `roles/redis.viewer` | Cloud Memorystore Redis discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/redis#redis.viewer) | +| `roles/resourcemanager.tagViewer` | Resource Manager tag discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/resourcemanager#resourcemanager.tagViewer) | +| `roles/run.viewer` | Cloud Run resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/run#run.viewer) | +| `roles/secretmanager.viewer` | Secret Manager secret discovery (metadata only) [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/secretmanager#secretmanager.viewer) | +| `roles/securitycentermanagement.viewer` | Security Center management discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/securitycentermanagement#securitycentermanagement.viewer) | +| `roles/servicedirectory.viewer` | Service Directory resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/servicedirectory#servicedirectory.viewer) | +| `roles/serviceusage.serviceUsageViewer` | Service Usage discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/serviceusage#serviceusage.serviceUsageViewer) | +| `roles/spanner.viewer` | Cloud Spanner resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/spanner#spanner.viewer) | +| `roles/storage.bucketViewer` | Cloud Storage bucket discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/storage#storage.bucketViewer) | +| `roles/storagetransfer.viewer` | Storage Transfer Service discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/storagetransfer#storagetransfer.viewer) | + +### Custom Role + +| Role | Purpose | +| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `projects/{PROJECT_ID}/roles/overmindCustomRole` | Custom role for additional BigQuery and Spanner permissions **Permissions:** `bigquery.transfers.get` - BigQuery transfer configuration discovery, `spanner.databases.get` - Spanner database detail discovery, `spanner.databases.list` - Spanner database enumeration | + +All predefined roles provide read-only access and are sourced from Google Cloud's [predefined roles documentation](https://cloud.google.com/iam/docs/understanding-roles#predefined). + +**Project-Level Restrictions:** Some roles (`roles/iam.roleViewer` and `roles/iam.serviceAccountViewer`) can only be granted at the project level in GCP. When configuring at the organization or folder level, these roles are automatically excluded. The custom role is also only created and assigned when using a project-level parent (e.g., `projects/my-project`). diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-batch-prediction-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-batch-prediction-job.json new file mode 100644 index 00000000..967b9b4b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-batch-prediction-job.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-ai-platform-batch-prediction-job", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-model", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Ai Platform Batch Prediction Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-batch-prediction-job by its \"locations|batchPredictionJobs\"", + "search": true, + "searchDescription": "Search Batch Prediction Jobs within a location. Use the location name e.g., 'us-central1'" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-custom-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-custom-job.json new file mode 100644 index 00000000..22003f70 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-custom-job.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-ai-platform-custom-job", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-model", + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Ai Platform Custom Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-custom-job by its \"name\"", + "list": true, + "listDescription": "List all gcp-ai-platform-custom-job" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-endpoint.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-endpoint.json new file mode 100644 index 00000000..67f77a9c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-endpoint.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-ai-platform-endpoint", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-model", + "gcp-ai-platform-model-deployment-monitoring-job", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key", + "gcp-compute-network" + ], + "descriptiveName": "GCP Ai Platform Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-endpoint by its \"name\"", + "list": true, + "listDescription": "List all gcp-ai-platform-endpoint" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model-deployment-monitoring-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model-deployment-monitoring-job.json new file mode 100644 index 00000000..5d1e6d04 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model-deployment-monitoring-job.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-ai-platform-model-deployment-monitoring-job", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-endpoint", + "gcp-ai-platform-model", + "gcp-cloud-kms-crypto-key", + "gcp-monitoring-notification-channel" + ], + "descriptiveName": "GCP Ai Platform Model Deployment Monitoring Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-model-deployment-monitoring-job by its \"locations|modelDeploymentMonitoringJobs\"", + "search": true, + "searchDescription": "Search Model Deployment Monitoring Jobs within a location. Use the location name e.g., 'us-central1'" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model.json new file mode 100644 index 00000000..a0ccd633 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-ai-platform-model", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-endpoint", + "gcp-ai-platform-pipeline-job", + "gcp-artifact-registry-docker-image", + "gcp-cloud-kms-crypto-key" + ], + "descriptiveName": "GCP Ai Platform Model", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-model by its \"name\"", + "list": true, + "listDescription": "List all gcp-ai-platform-model" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-pipeline-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-pipeline-job.json new file mode 100644 index 00000000..48d48bc7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-pipeline-job.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-ai-platform-pipeline-job", + "category": 8, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Ai Platform Pipeline Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-pipeline-job by its \"name\"", + "list": true, + "listDescription": "List all gcp-ai-platform-pipeline-job" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-artifact-registry-docker-image.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-artifact-registry-docker-image.json new file mode 100644 index 00000000..1c51945e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-artifact-registry-docker-image.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-artifact-registry-docker-image", + "category": 2, + "descriptiveName": "GCP Artifact Registry Docker Image", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-artifact-registry-docker-image by its \"locations|repositories|dockerImages\"", + "search": true, + "searchDescription": "Search for Docker images in Artifact Registry. Use the format \"location|repository_id\" or \"projects/[project]/locations/[location]/repository/[repository_id]/dockerImages/[docker_image]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_artifact_registry_docker_image.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-data-transfer-transfer-config.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-data-transfer-transfer-config.json new file mode 100644 index 00000000..fa7b6a88 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-data-transfer-transfer-config.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-big-query-data-transfer-transfer-config", + "category": 6, + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-cloud-kms-crypto-key", + "gcp-pub-sub-topic" + ], + "descriptiveName": "GCP Big Query Data Transfer Transfer Config", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-query-data-transfer-transfer-config by its \"locations|transferConfigs\"", + "search": true, + "searchDescription": "Search for BigQuery Data Transfer transfer configs in a location. Use the format \"location\" or \"projects/project_id/locations/location/transferConfigs/transfer_config_id\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigquery_data_transfer_config.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-dataset.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-dataset.json new file mode 100644 index 00000000..3efc5539 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-dataset.json @@ -0,0 +1,24 @@ +{ + "type": "gcp-big-query-dataset", + "category": 6, + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-big-query-model", + "gcp-big-query-routine", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account" + ], + "descriptiveName": "GCP Big Query Dataset", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Big Query Dataset by \"gcp-big-query-dataset-id\"", + "list": true, + "listDescription": "List all GCP Big Query Dataset items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_bigquery_dataset.dataset_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-model.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-model.json new file mode 100644 index 00000000..be1751c4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-model.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-big-query-model", + "category": 6, + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key" + ], + "descriptiveName": "GCP Big Query Model", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Big Query Model by \"gcp-big-query-dataset-id|gcp-big-query-model-id\"", + "search": true, + "searchDescription": "Search for GCP Big Query Model by \"gcp-big-query-model-id\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-routine.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-routine.json new file mode 100644 index 00000000..b9704125 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-routine.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-big-query-routine", + "category": 6, + "potentialLinks": ["gcp-big-query-dataset"], + "descriptiveName": "GCP Big Query Routine", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Big Query Routine by \"gcp-big-query-dataset-id|gcp-big-query-routine-id\"", + "search": true, + "searchDescription": "Search for GCP Big Query Routine by \"gcp-big-query-routine-id\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_bigquery_routine.routine_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-table.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-table.json new file mode 100644 index 00000000..6c9b01c6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-table.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-big-query-table", + "category": 6, + "potentialLinks": ["gcp-big-query-dataset", "gcp-cloud-kms-crypto-key"], + "descriptiveName": "GCP Big Query Table", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Big Query Table by \"gcp-big-query-dataset-id|gcp-big-query-table-id\"", + "search": true, + "searchDescription": "Search for GCP Big Query Table by \"gcp-big-query-dataset-id\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_bigquery_table.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-app-profile.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-app-profile.json new file mode 100644 index 00000000..4358f119 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-app-profile.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-big-table-admin-app-profile", + "category": 7, + "potentialLinks": [ + "gcp-big-table-admin-cluster", + "gcp-big-table-admin-instance" + ], + "descriptiveName": "GCP Big Table Admin App Profile", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-app-profile by its \"instances|appProfiles\"", + "search": true, + "searchDescription": "Search for BigTable App Profiles in an instance. Use the format \"instance\" or \"projects/[project_id]/instances/[instance_name]/appProfiles/[app_profile_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigtable_app_profile.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-backup.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-backup.json new file mode 100644 index 00000000..3f8518c8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-backup.json @@ -0,0 +1,15 @@ +{ + "type": "gcp-big-table-admin-backup", + "potentialLinks": [ + "gcp-big-table-admin-backup", + "gcp-big-table-admin-cluster", + "gcp-big-table-admin-table" + ], + "descriptiveName": "GCP Big Table Admin Backup", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-backup by its \"instances|clusters|backups\"", + "search": true, + "searchDescription": "Search for gcp-big-table-admin-backup by its \"instances|clusters\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-cluster.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-cluster.json new file mode 100644 index 00000000..bb3be7f6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-cluster.json @@ -0,0 +1,15 @@ +{ + "type": "gcp-big-table-admin-cluster", + "category": 7, + "potentialLinks": [ + "gcp-big-table-admin-instance", + "gcp-cloud-kms-crypto-key" + ], + "descriptiveName": "GCP Big Table Admin Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-cluster by its \"instances|clusters\"", + "search": true, + "searchDescription": "Search for gcp-big-table-admin-cluster by its \"instances\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-instance.json new file mode 100644 index 00000000..fabaff46 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-instance.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-big-table-admin-instance", + "category": 7, + "potentialLinks": ["gcp-big-table-admin-cluster"], + "descriptiveName": "GCP Big Table Admin Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-instance by its \"name\"", + "list": true, + "listDescription": "List all gcp-big-table-admin-instance" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_bigtable_instance.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-table.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-table.json new file mode 100644 index 00000000..f490a956 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-table.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-big-table-admin-table", + "category": 6, + "potentialLinks": [ + "gcp-big-table-admin-backup", + "gcp-big-table-admin-instance", + "gcp-big-table-admin-table" + ], + "descriptiveName": "GCP Big Table Admin Table", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-table by its \"instances|tables\"", + "search": true, + "searchDescription": "Search for BigTable tables in an instance. Use the format \"instance_name\" or \"projects/[project_id]/instances/[instance_name]/tables/[table_name]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigtable_table.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-billing-billing-info.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-billing-billing-info.json new file mode 100644 index 00000000..d34a018f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-billing-billing-info.json @@ -0,0 +1,10 @@ +{ + "type": "gcp-cloud-billing-billing-info", + "category": 7, + "potentialLinks": ["gcp-cloud-resource-manager-project"], + "descriptiveName": "GCP Cloud Billing Billing Info", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-billing-billing-info by its \"name\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-build-build.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-build-build.json new file mode 100644 index 00000000..234cabf0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-build-build.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-cloud-build-build", + "category": 7, + "potentialLinks": [ + "gcp-artifact-registry-docker-image", + "gcp-iam-service-account", + "gcp-logging-bucket", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Cloud Build Build", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-build-build by its \"name\"", + "list": true, + "listDescription": "List all gcp-cloud-build-build" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-functions-function.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-functions-function.json new file mode 100644 index 00000000..aa542ad7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-functions-function.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-cloud-functions-function", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account", + "gcp-pub-sub-topic", + "gcp-run-service", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Cloud Functions Function", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-functions-function by its \"locations|functions\"", + "search": true, + "searchDescription": "Search for gcp-cloud-functions-function by its \"locations\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key.json new file mode 100644 index 00000000..c28ec0de --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-cloud-kms-crypto-key", + "category": 4, + "potentialLinks": ["gcp-cloud-kms-key-ring"], + "descriptiveName": "GCP Cloud Kms Crypto Key", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Cloud Kms Crypto Key by \"gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name\"", + "search": true, + "searchDescription": "Search for GCP Cloud Kms Crypto Key by \"gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-key-ring.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-key-ring.json new file mode 100644 index 00000000..6eeaa47c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-key-ring.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-cloud-kms-key-ring", + "category": 4, + "potentialLinks": ["gcp-cloud-kms-crypto-key"], + "descriptiveName": "GCP Cloud Kms Key Ring", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Cloud Kms Key Ring by \"gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name\"", + "search": true, + "searchDescription": "Search for GCP Cloud Kms Key Ring by \"gcp-cloud-kms-key-ring-location\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_kms_key_ring.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-project.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-project.json new file mode 100644 index 00000000..61a6eb0b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-project.json @@ -0,0 +1,9 @@ +{ + "type": "gcp-cloud-resource-manager-project", + "category": 7, + "descriptiveName": "GCP Cloud Resource Manager Project", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-resource-manager-project by its \"name\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-tag-value.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-tag-value.json new file mode 100644 index 00000000..48c3491c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-tag-value.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-cloud-resource-manager-tag-value", + "category": 7, + "descriptiveName": "GCP Cloud Resource Manager Tag Value", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-resource-manager-tag-value by its \"name\"", + "search": true, + "searchDescription": "Search for TagValues by TagKey." + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_tags_tag_value.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json new file mode 100644 index 00000000..53ebe1ef --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-address", + "category": 3, + "potentialLinks": [ + "gcp-compute-address", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Address", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Address by \"gcp-compute-address-name\"", + "list": true, + "listDescription": "List all GCP Compute Address items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_address.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json new file mode 100644 index 00000000..41effcaf --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-autoscaler", + "category": 7, + "potentialLinks": ["gcp-compute-instance-group-manager"], + "descriptiveName": "GCP Compute Autoscaler", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Autoscaler by \"gcp-compute-autoscaler-name\"", + "list": true, + "listDescription": "List all GCP Compute Autoscaler items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_autoscaler.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-backend-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-backend-service.json new file mode 100644 index 00000000..c103843e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-backend-service.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-backend-service", + "category": 1, + "potentialLinks": ["gcp-compute-network", "gcp-compute-security-policy"], + "descriptiveName": "GCP Compute Backend Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Backend Service by \"gcp-compute-backend-service-name\"", + "list": true, + "listDescription": "List all GCP Compute Backend Service items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_backend_service.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json new file mode 100644 index 00000000..435b2c8c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-compute-disk", + "category": 2, + "potentialLinks": [ + "gcp-compute-disk", + "gcp-compute-image", + "gcp-compute-instance", + "gcp-compute-instant-snapshot", + "gcp-compute-snapshot" + ], + "descriptiveName": "GCP Compute Disk", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Disk by \"gcp-compute-disk-name\"", + "list": true, + "listDescription": "List all GCP Compute Disk items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_disk.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-external-vpn-gateway.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-external-vpn-gateway.json new file mode 100644 index 00000000..2ee555ba --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-external-vpn-gateway.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-external-vpn-gateway", + "category": 3, + "descriptiveName": "GCP Compute External Vpn Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-external-vpn-gateway by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-external-vpn-gateway" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_external_vpn_gateway.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json new file mode 100644 index 00000000..6dd11c29 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-firewall", + "category": 3, + "potentialLinks": ["gcp-compute-network", "gcp-iam-service-account"], + "descriptiveName": "GCP Compute Firewall", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-firewall by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-firewall" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_firewall.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-forwarding-rule.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-forwarding-rule.json new file mode 100644 index 00000000..bd2ce7de --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-forwarding-rule.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-forwarding-rule", + "category": 3, + "potentialLinks": [ + "gcp-compute-backend-service", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Forwarding Rule", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Forwarding Rule by \"gcp-compute-forwarding-rule-name\"", + "list": true, + "listDescription": "List all GCP Compute Forwarding Rule items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_forwarding_rule.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-address.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-address.json new file mode 100644 index 00000000..84db32de --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-address.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-global-address", + "category": 3, + "potentialLinks": ["gcp-compute-network"], + "descriptiveName": "GCP Compute Global Address", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-global-address by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-global-address" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_global_address.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-forwarding-rule.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-forwarding-rule.json new file mode 100644 index 00000000..28c37fc0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-forwarding-rule.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-global-forwarding-rule", + "category": 3, + "potentialLinks": [ + "gcp-compute-backend-service", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Global Forwarding Rule", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-global-forwarding-rule by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-global-forwarding-rule" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_global_forwarding_rule.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-health-check.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-health-check.json new file mode 100644 index 00000000..beda2c95 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-health-check.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-health-check", + "category": 7, + "descriptiveName": "GCP Compute Health Check", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Health Check by \"gcp-compute-health-check-name\"", + "list": true, + "listDescription": "List all GCP Compute Health Check items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_health_check.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-http-health-check.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-http-health-check.json new file mode 100644 index 00000000..f5d21e72 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-http-health-check.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-http-health-check", + "category": 3, + "descriptiveName": "GCP Compute Http Health Check", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-http-health-check by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-http-health-check" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_http_health_check.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json new file mode 100644 index 00000000..acf4c7a2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-image", + "category": 1, + "descriptiveName": "GCP Compute Image", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Image by \"gcp-compute-image-name\"", + "list": true, + "listDescription": "List all GCP Compute Image items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_image.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group-manager.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group-manager.json new file mode 100644 index 00000000..bc80be40 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group-manager.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-compute-instance-group-manager", + "category": 1, + "potentialLinks": [ + "gcp-compute-autoscaler", + "gcp-compute-instance-group", + "gcp-compute-instance-template", + "gcp-compute-target-pool" + ], + "descriptiveName": "GCP Compute Instance Group Manager", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Instance Group Manager by \"gcp-compute-instance-group-manager-name\"", + "list": true, + "listDescription": "List all GCP Compute Instance Group Manager items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instance_group_manager.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group.json new file mode 100644 index 00000000..0368fa88 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-instance-group", + "category": 1, + "potentialLinks": ["gcp-compute-network", "gcp-compute-subnetwork"], + "descriptiveName": "GCP Compute Instance Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Instance Group by \"gcp-compute-instance-group-name\"", + "list": true, + "listDescription": "List all GCP Compute Instance Group items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instance_group.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-template.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-template.json new file mode 100644 index 00000000..b56bff5c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-template.json @@ -0,0 +1,29 @@ +{ + "type": "gcp-compute-instance-template", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-disk", + "gcp-compute-image", + "gcp-compute-instance", + "gcp-compute-network", + "gcp-compute-node-group", + "gcp-compute-reservation", + "gcp-compute-security-policy", + "gcp-compute-snapshot", + "gcp-compute-subnetwork", + "gcp-iam-service-account" + ], + "descriptiveName": "GCP Compute Instance Template", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-instance-template by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-instance-template" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instance_template.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json new file mode 100644 index 00000000..b4700f94 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-instance", + "category": 1, + "potentialLinks": [ + "gcp-compute-disk", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Instance by \"gcp-compute-instance-name\"", + "list": true, + "listDescription": "List all GCP Compute Instance items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instance.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instant-snapshot.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instant-snapshot.json new file mode 100644 index 00000000..a37e8bd6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instant-snapshot.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-instant-snapshot", + "category": 2, + "potentialLinks": ["gcp-compute-disk"], + "descriptiveName": "GCP Compute Instant Snapshot", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Instant Snapshot by \"gcp-compute-instant-snapshot-name\"", + "list": true, + "listDescription": "List all GCP Compute Instant Snapshot items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instant_snapshot.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-machine-image.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-machine-image.json new file mode 100644 index 00000000..72970664 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-machine-image.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-compute-machine-image", + "category": 1, + "potentialLinks": [ + "gcp-compute-disk", + "gcp-compute-instance", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Machine Image", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Machine Image by \"gcp-compute-machine-image-name\"", + "list": true, + "listDescription": "List all GCP Compute Machine Image items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_machine_image.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network-endpoint-group.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network-endpoint-group.json new file mode 100644 index 00000000..cf4fa2ae --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network-endpoint-group.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-compute-network-endpoint-group", + "category": 3, + "potentialLinks": [ + "gcp-cloud-functions-function", + "gcp-compute-network", + "gcp-compute-subnetwork", + "gcp-run-service" + ], + "descriptiveName": "GCP Compute Network Endpoint Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-network-endpoint-group by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-network-endpoint-group" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_network_endpoint_group.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json new file mode 100644 index 00000000..f4e5d730 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-network", + "category": 3, + "potentialLinks": ["gcp-compute-network", "gcp-compute-subnetwork"], + "descriptiveName": "GCP Compute Network", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-network by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-network" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_network.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-group.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-group.json new file mode 100644 index 00000000..73f3c8c0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-group.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-compute-node-group", + "category": 1, + "descriptiveName": "GCP Compute Node Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Node Group by \"gcp-compute-node-group-name\"", + "list": true, + "listDescription": "List all GCP Compute Node Group items", + "search": true, + "searchDescription": "Search for GCP Compute Node Group by \"gcp-compute-node-group-nodeTemplateName\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_node_group.name" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "google_compute_node_template.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json new file mode 100644 index 00000000..b6c4fd2b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json @@ -0,0 +1,10 @@ +{ + "type": "gcp-compute-project", + "category": 7, + "potentialLinks": ["gcp-iam-service-account", "gcp-storage-bucket"], + "descriptiveName": "GCP Compute Project", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-project by its \"name\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-public-delegated-prefix.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-public-delegated-prefix.json new file mode 100644 index 00000000..7348176c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-public-delegated-prefix.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-compute-public-delegated-prefix", + "category": 3, + "potentialLinks": [ + "gcp-cloud-resource-manager-project", + "gcp-compute-public-delegated-prefix" + ], + "descriptiveName": "GCP Compute Public Delegated Prefix", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-public-delegated-prefix by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-public-delegated-prefix", + "search": true, + "searchDescription": "Search with full ID: projects/[project]/regions/[region]/publicDelegatedPrefixes/[name] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_compute_public_delegated_prefix.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-backend-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-backend-service.json new file mode 100644 index 00000000..81449263 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-backend-service.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-region-backend-service", + "category": 1, + "potentialLinks": [ + "gcp-compute-instance-group", + "gcp-compute-network", + "gcp-compute-security-policy" + ], + "descriptiveName": "GCP Compute Region Backend Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Region Backend Service by \"gcp-compute-region-backend-service-name\"", + "list": true, + "listDescription": "List all GCP Compute Region Backend Service items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_region_backend_service.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-commitment.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-commitment.json new file mode 100644 index 00000000..56244bf7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-commitment.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-region-commitment", + "potentialLinks": ["gcp-compute-reservation"], + "descriptiveName": "GCP Compute Region Commitment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-region-commitment by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-region-commitment" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_region_commitment.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json new file mode 100644 index 00000000..6ea4f52b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json @@ -0,0 +1,19 @@ +{ + "type": "gcp-compute-reservation", + "category": 1, + "potentialLinks": [ + "gcp-compute-region-commitment" + ], + "descriptiveName": "GCP Compute Reservation", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Reservation by \"gcp-compute-reservation-name\"", + "list": true, + "listDescription": "List all GCP Compute Reservation items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_reservation.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json new file mode 100644 index 00000000..5a2ebefc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-route", + "category": 3, + "potentialLinks": [ + "gcp-compute-instance", + "gcp-compute-network", + "gcp-compute-vpn-tunnel" + ], + "descriptiveName": "GCP Compute Route", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-route by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-route" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_route.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json new file mode 100644 index 00000000..525bec37 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json @@ -0,0 +1,24 @@ +{ + "type": "gcp-compute-router", + "category": 3, + "potentialLinks": [ + "gcp-compute-network", + "gcp-compute-subnetwork", + "gcp-compute-vpn-tunnel" + ], + "descriptiveName": "GCP Compute Router", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-router by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-router", + "search": true, + "searchDescription": "Search with full ID: projects/[project]/regions/[region]/routers/[router] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_compute_router.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-security-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-security-policy.json new file mode 100644 index 00000000..b2a5b7e5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-security-policy.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-security-policy", + "category": 4, + "descriptiveName": "GCP Compute Security Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Security Policy by \"gcp-compute-security-policy-name\"", + "list": true, + "listDescription": "List all GCP Compute Security Policy items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_security_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json new file mode 100644 index 00000000..5b42d808 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-snapshot", + "category": 2, + "potentialLinks": ["gcp-compute-disk", "gcp-compute-instant-snapshot"], + "descriptiveName": "GCP Compute Snapshot", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Snapshot by \"gcp-compute-snapshot-name\"", + "list": true, + "listDescription": "List all GCP Compute Snapshot items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_snapshot.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-certificate.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-certificate.json new file mode 100644 index 00000000..d2b248fc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-certificate.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-ssl-certificate", + "category": 7, + "descriptiveName": "GCP Compute Ssl Certificate", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-ssl-certificate by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-ssl-certificate" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_ssl_certificate.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-policy.json new file mode 100644 index 00000000..37962175 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-policy.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-ssl-policy", + "category": 4, + "descriptiveName": "GCP Compute Ssl Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-ssl-policy by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-ssl-policy" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_ssl_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json new file mode 100644 index 00000000..2af0ff8e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-subnetwork", + "category": 3, + "potentialLinks": ["gcp-compute-network"], + "descriptiveName": "GCP Compute Subnetwork", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-subnetwork by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-subnetwork" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_subnetwork.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-http-proxy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-http-proxy.json new file mode 100644 index 00000000..866c2fe2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-http-proxy.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-target-http-proxy", + "category": 3, + "potentialLinks": ["gcp-compute-url-map"], + "descriptiveName": "GCP Compute Target Http Proxy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-target-http-proxy by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-target-http-proxy" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_target_http_proxy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-https-proxy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-https-proxy.json new file mode 100644 index 00000000..329ff140 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-https-proxy.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-target-https-proxy", + "category": 3, + "potentialLinks": [ + "gcp-compute-ssl-certificate", + "gcp-compute-ssl-policy", + "gcp-compute-url-map" + ], + "descriptiveName": "GCP Compute Target Https Proxy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-target-https-proxy by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-target-https-proxy" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_target_https_proxy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-pool.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-pool.json new file mode 100644 index 00000000..d4af55ad --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-pool.json @@ -0,0 +1,24 @@ +{ + "type": "gcp-compute-target-pool", + "category": 3, + "potentialLinks": [ + "gcp-compute-health-check", + "gcp-compute-instance", + "gcp-compute-target-pool" + ], + "descriptiveName": "GCP Compute Target Pool", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-target-pool by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-target-pool", + "search": true, + "searchDescription": "Search with full ID: projects/[project]/regions/[region]/targetPools/[name] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_compute_target_pool.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-url-map.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-url-map.json new file mode 100644 index 00000000..e5ae1cc4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-url-map.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-url-map", + "category": 3, + "potentialLinks": ["gcp-compute-backend-service"], + "descriptiveName": "GCP Compute Url Map", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-url-map by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-url-map" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_url_map.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-gateway.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-gateway.json new file mode 100644 index 00000000..29de0dae --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-gateway.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-vpn-gateway", + "category": 3, + "potentialLinks": ["gcp-compute-network"], + "descriptiveName": "GCP Compute Vpn Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-vpn-gateway by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-vpn-gateway" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_ha_vpn_gateway.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-tunnel.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-tunnel.json new file mode 100644 index 00000000..10d8198b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-tunnel.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-vpn-tunnel", + "category": 3, + "potentialLinks": [ + "gcp-compute-external-vpn-gateway", + "gcp-compute-router", + "gcp-compute-vpn-gateway" + ], + "descriptiveName": "GCP Compute Vpn Tunnel", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-vpn-tunnel by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-vpn-tunnel" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_vpn_tunnel.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json new file mode 100644 index 00000000..75607613 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json @@ -0,0 +1,26 @@ +{ + "type": "gcp-container-cluster", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-compute-node-group", + "gcp-compute-subnetwork", + "gcp-container-node-pool", + "gcp-iam-service-account", + "gcp-pub-sub-topic" + ], + "descriptiveName": "GCP Container Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-container-cluster by its \"locations|clusters\"", + "search": true, + "searchDescription": "Search for GKE clusters in a location. Use the format \"location\" or the full resource name supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_container_cluster.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-container-node-pool.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-node-pool.json new file mode 100644 index 00000000..d14c9319 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-node-pool.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-container-node-pool", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-node-group", + "gcp-container-cluster", + "gcp-iam-service-account" + ], + "descriptiveName": "GCP Container Node Pool", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-container-node-pool by its \"locations|clusters|nodePools\"", + "search": true, + "searchDescription": "Search GKE Node Pools within a cluster. Use \"[location]|[cluster]\" or the full resource name supported by Terraform mappings: \"[project]/[location]/[cluster]/[node_pool_name]\"" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_container_node_pool.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json new file mode 100644 index 00000000..0513d8c6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-dataform-repository", + "category": 6, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account", + "gcp-secret-manager-secret" + ], + "descriptiveName": "GCP Dataform Repository", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataform-repository by its \"locations|repositories\"", + "search": true, + "searchDescription": "Search for Dataform repositories in a location. Use the format \"location\" or \"projects/[project_id]/locations/[location]/repositories/[repository_name]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_dataform_repository.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-aspect-type.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-aspect-type.json new file mode 100644 index 00000000..9d600015 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-aspect-type.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-dataplex-aspect-type", + "category": 7, + "descriptiveName": "GCP Dataplex Aspect Type", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataplex-aspect-type by its \"locations|aspectTypes\"", + "search": true, + "searchDescription": "Search for Dataplex aspect types in a location. Use the format \"location\" or \"projects/[project_id]/locations/[location]/aspectTypes/[aspect_type_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_dataplex_aspect_type.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-data-scan.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-data-scan.json new file mode 100644 index 00000000..99ae75d0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-data-scan.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-dataplex-data-scan", + "category": 5, + "potentialLinks": ["gcp-storage-bucket"], + "descriptiveName": "GCP Dataplex Data Scan", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataplex-data-scan by its \"locations|dataScans\"", + "search": true, + "searchDescription": "Search for Dataplex data scans in a location. Use the location name e.g., 'us-central1' or the format \"projects/[project_id]/locations/[location]/dataScans/[data_scan_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_dataplex_datascan.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-entry-group.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-entry-group.json new file mode 100644 index 00000000..22f88da0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-entry-group.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-dataplex-entry-group", + "category": 2, + "descriptiveName": "GCP Dataplex Entry Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataplex-entry-group by its \"locations|entryGroups\"", + "search": true, + "searchDescription": "Search for Dataplex entry groups in a location. Use the format \"location\" or \"projects/[project_id]/locations/[location]/entryGroups/[entry_group_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_dataplex_entry_group.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-autoscaling-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-autoscaling-policy.json new file mode 100644 index 00000000..05e71e51 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-autoscaling-policy.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-dataproc-autoscaling-policy", + "category": 7, + "descriptiveName": "GCP Dataproc Autoscaling Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataproc-autoscaling-policy by its \"name\"", + "list": true, + "listDescription": "List all gcp-dataproc-autoscaling-policy" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_dataproc_autoscaling_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json new file mode 100644 index 00000000..d1f19d26 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json @@ -0,0 +1,27 @@ +{ + "type": "gcp-dataproc-cluster", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-image", + "gcp-compute-instance-group-manager", + "gcp-compute-network", + "gcp-compute-node-group", + "gcp-compute-subnetwork", + "gcp-dataproc-autoscaling-policy", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Dataproc Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataproc-cluster by its \"name\"", + "list": true, + "listDescription": "List all gcp-dataproc-cluster" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_dataproc_cluster.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dns-managed-zone.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dns-managed-zone.json new file mode 100644 index 00000000..54e61a26 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dns-managed-zone.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-dns-managed-zone", + "category": 3, + "potentialLinks": ["gcp-compute-network", "gcp-container-cluster"], + "descriptiveName": "GCP Dns Managed Zone", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dns-managed-zone by its \"name\"", + "list": true, + "listDescription": "List all gcp-dns-managed-zone" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_dns_managed_zone.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-essential-contacts-contact.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-essential-contacts-contact.json new file mode 100644 index 00000000..da7a9ff2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-essential-contacts-contact.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-essential-contacts-contact", + "descriptiveName": "GCP Essential Contacts Contact", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-essential-contacts-contact by its \"name\"", + "list": true, + "listDescription": "List all gcp-essential-contacts-contact", + "search": true, + "searchDescription": "Search for contacts by their ID in the form of \"projects/[project_id]/contacts/[contact_id]\"." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_essential_contacts_contact.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json new file mode 100644 index 00000000..1a538cc3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-file-instance", + "category": 2, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-compute-network"], + "descriptiveName": "GCP File Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-file-instance by its \"locations|instances\"", + "search": true, + "searchDescription": "Search for Filestore instances in a location. Use the location string or the full resource name supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_filestore_instance.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json new file mode 100644 index 00000000..1f4396c5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json @@ -0,0 +1,11 @@ +{ + "type": "gcp-iam-role", + "category": 4, + "descriptiveName": "GCP Iam Role", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-iam-role by its \"name\"", + "list": true, + "listDescription": "List all gcp-iam-role" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account-key.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account-key.json new file mode 100644 index 00000000..07a30c6a --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account-key.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-iam-service-account-key", + "category": 4, + "potentialLinks": ["gcp-iam-service-account"], + "descriptiveName": "GCP Iam Service Account Key", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Iam Service Account Key by \"gcp-iam-service-account-email or unique_id|gcp-iam-service-account-key-name\"", + "search": true, + "searchDescription": "Search for GCP Iam Service Account Key by \"gcp-iam-service-account-email or unique_id\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_service_account_key.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account.json new file mode 100644 index 00000000..f4e8078e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-iam-service-account", + "category": 4, + "potentialLinks": [ + "gcp-cloud-resource-manager-project", + "gcp-iam-service-account-key" + ], + "descriptiveName": "GCP Iam Service Account", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Iam Service Account by \"gcp-iam-service-account-email or unique_id\"", + "list": true, + "listDescription": "List all GCP Iam Service Account items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_service_account.email" + }, + { + "terraformQueryMap": "google_service_account.unique_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json new file mode 100644 index 00000000..e63a32f8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-logging-bucket", + "category": 5, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-iam-service-account"], + "descriptiveName": "GCP Logging Bucket", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-logging-bucket by its \"locations|buckets\"", + "search": true, + "searchDescription": "Search for gcp-logging-bucket by its \"locations\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json new file mode 100644 index 00000000..319a71a0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-logging-link", + "category": 5, + "potentialLinks": ["gcp-big-query-dataset", "gcp-logging-bucket"], + "descriptiveName": "GCP Logging Link", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-logging-link by its \"locations|buckets|links\"", + "search": true, + "searchDescription": "Search for gcp-logging-link by its \"locations|buckets\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-saved-query.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-saved-query.json new file mode 100644 index 00000000..f68dd359 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-saved-query.json @@ -0,0 +1,11 @@ +{ + "type": "gcp-logging-saved-query", + "category": 5, + "descriptiveName": "GCP Logging Saved Query", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-logging-saved-query by its \"locations|savedQueries\"", + "search": true, + "searchDescription": "Search for gcp-logging-saved-query by its \"locations\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json new file mode 100644 index 00000000..013bcc3b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-logging-sink", + "category": 7, + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-logging-bucket", + "gcp-pub-sub-topic", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Logging Sink", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Logging Sink by \"gcp-logging-sink-name\"", + "list": true, + "listDescription": "List all GCP Logging Sink items" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-alert-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-alert-policy.json new file mode 100644 index 00000000..89297235 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-alert-policy.json @@ -0,0 +1,20 @@ +{ + "type": "gcp-monitoring-alert-policy", + "category": 5, + "potentialLinks": ["gcp-monitoring-notification-channel"], + "descriptiveName": "GCP Monitoring Alert Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-monitoring-alert-policy by its \"name\"", + "list": true, + "listDescription": "List all gcp-monitoring-alert-policy", + "search": true, + "searchDescription": "Search by full resource name: projects/[project]/alertPolicies/[alert_policy_id] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_monitoring_alert_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-custom-dashboard.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-custom-dashboard.json new file mode 100644 index 00000000..5dcb1ef2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-custom-dashboard.json @@ -0,0 +1,19 @@ +{ + "type": "gcp-monitoring-custom-dashboard", + "category": 5, + "descriptiveName": "GCP Monitoring Custom Dashboard", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-monitoring-custom-dashboard by its \"name\"", + "list": true, + "listDescription": "List all gcp-monitoring-custom-dashboard", + "search": true, + "searchDescription": "Search for custom dashboards by their ID in the form of \"projects/[project_id]/dashboards/[dashboard_id]\". This is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_monitoring_dashboard.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-notification-channel.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-notification-channel.json new file mode 100644 index 00000000..9da3e095 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-notification-channel.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-monitoring-notification-channel", + "category": 5, + "descriptiveName": "GCP Monitoring Notification Channel", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-monitoring-notification-channel by its \"name\"", + "list": true, + "listDescription": "List all gcp-monitoring-notification-channel", + "search": true, + "searchDescription": "Search by full resource name: projects/[project]/notificationChannels/[notificationChannel] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_monitoring_notification_channel.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json new file mode 100644 index 00000000..0126fcb8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json @@ -0,0 +1,19 @@ +{ + "type": "gcp-orgpolicy-policy", + "category": 7, + "descriptiveName": "GCP Orgpolicy Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-orgpolicy-policy by its \"name\"", + "list": true, + "listDescription": "List all gcp-orgpolicy-policy", + "search": true, + "searchDescription": "Search with the full policy name: projects/[project]/policies/[constraint] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_org_policy_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-subscription.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-subscription.json new file mode 100644 index 00000000..35abc174 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-subscription.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-pub-sub-subscription", + "category": 7, + "potentialLinks": [ + "gcp-big-query-table", + "gcp-pub-sub-subscription", + "gcp-pub-sub-topic", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Pub Sub Subscription", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-pub-sub-subscription by its \"name\"", + "list": true, + "listDescription": "List all gcp-pub-sub-subscription" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_pubsub_subscription.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-topic.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-topic.json new file mode 100644 index 00000000..7f8a34b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-topic.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-pub-sub-topic", + "category": 7, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-storage-bucket"], + "descriptiveName": "GCP Pub Sub Topic", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-pub-sub-topic by its \"name\"", + "list": true, + "listDescription": "List all gcp-pub-sub-topic" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_pubsub_topic.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json new file mode 100644 index 00000000..96b1fcd6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-redis-instance", + "category": 6, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-compute-ssl-certificate" + ], + "descriptiveName": "GCP Redis Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-redis-instance by its \"locations|instances\"", + "search": true, + "searchDescription": "Search Redis instances in a location. Use the format \"location\" or \"projects/[project_id]/locations/[location]/instances/[instance_name]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_redis_instance.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json new file mode 100644 index 00000000..44d783b7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-run-revision", + "category": 7, + "potentialLinks": [ + "gcp-artifact-registry-docker-image", + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-compute-subnetwork", + "gcp-iam-service-account", + "gcp-run-service", + "gcp-sql-admin-instance", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Run Revision", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-run-revision by its \"locations|services|revisions\"", + "search": true, + "searchDescription": "Search for gcp-run-revision by its \"locations|services\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json new file mode 100644 index 00000000..74851f26 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json @@ -0,0 +1,28 @@ +{ + "type": "gcp-run-service", + "category": 1, + "potentialLinks": [ + "gcp-artifact-registry-docker-image", + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-compute-subnetwork", + "gcp-iam-service-account", + "gcp-run-revision", + "gcp-secret-manager-secret", + "gcp-sql-admin-instance", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Run Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-run-service by its \"locations|services\"", + "search": true, + "searchDescription": "Search for gcp-run-service by its \"locations\"" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_cloud_run_v2_service.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-secret-manager-secret.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-secret-manager-secret.json new file mode 100644 index 00000000..c85bf1d8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-secret-manager-secret.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-secret-manager-secret", + "category": 4, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-pub-sub-topic"], + "descriptiveName": "GCP Secret Manager Secret", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-secret-manager-secret by its \"name\"", + "list": true, + "listDescription": "List all gcp-secret-manager-secret" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_secret_manager_secret.secret_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-security-center-management-security-center-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-security-center-management-security-center-service.json new file mode 100644 index 00000000..f5c6408f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-security-center-management-security-center-service.json @@ -0,0 +1,11 @@ +{ + "type": "gcp-security-center-management-security-center-service", + "category": 4, + "descriptiveName": "GCP Security Center Management Security Center Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-security-center-management-security-center-service by its \"locations|securityCenterServices\"", + "search": true, + "searchDescription": "Search Security Center services in a location. Use the format \"location\"." + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-service-directory-endpoint.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-service-directory-endpoint.json new file mode 100644 index 00000000..83acd69d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-service-directory-endpoint.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-service-directory-endpoint", + "category": 7, + "potentialLinks": ["gcp-compute-network"], + "descriptiveName": "GCP Service Directory Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-service-directory-endpoint by its \"locations|namespaces|services|endpoints\"", + "search": true, + "searchDescription": "Search for endpoints by \"location|namespace_id|service_id\" or \"projects/[project_id]/locations/[location]/namespaces/[namespace_id]/services/[service_id]/endpoints/[endpoint_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_service_directory_endpoint.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-service-usage-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-service-usage-service.json new file mode 100644 index 00000000..8c15f219 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-service-usage-service.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-service-usage-service", + "category": 7, + "potentialLinks": ["gcp-pub-sub-topic"], + "descriptiveName": "GCP Service Usage Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-service-usage-service by its \"name\"", + "list": true, + "listDescription": "List all gcp-service-usage-service" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json new file mode 100644 index 00000000..f333a2dc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-spanner-database", + "category": 6, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-spanner-instance"], + "descriptiveName": "GCP Spanner Database", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-spanner-database by its \"instances|databases\"", + "search": true, + "searchDescription": "Search for gcp-spanner-database by its \"instances\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_spanner_database.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json new file mode 100644 index 00000000..b3193800 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-spanner-instance", + "category": 6, + "potentialLinks": ["gcp-spanner-database"], + "descriptiveName": "GCP Spanner Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-spanner-instance by its \"name\"", + "list": true, + "listDescription": "List all gcp-spanner-instance" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_spanner_instance.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup-run.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup-run.json new file mode 100644 index 00000000..1071747e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup-run.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-sql-admin-backup-run", + "category": 6, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-sql-admin-instance"], + "descriptiveName": "GCP Sql Admin Backup Run", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-sql-admin-backup-run by its \"instances|backupRuns\"", + "search": true, + "searchDescription": "Search for gcp-sql-admin-backup-run by its \"instances\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup.json new file mode 100644 index 00000000..474f8076 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-sql-admin-backup", + "category": 6, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-sql-admin-backup-run", + "gcp-sql-admin-instance" + ], + "descriptiveName": "GCP Sql Admin Backup", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-sql-admin-backup by its \"name\"", + "list": true, + "listDescription": "List all gcp-sql-admin-backup" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-instance.json new file mode 100644 index 00000000..16d72392 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-instance.json @@ -0,0 +1,24 @@ +{ + "type": "gcp-sql-admin-instance", + "category": 6, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-iam-service-account", + "gcp-sql-admin-backup-run", + "gcp-sql-admin-instance", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Sql Admin Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-sql-admin-instance by its \"name\"", + "list": true, + "listDescription": "List all gcp-sql-admin-instance" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_sql_database_instance.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json new file mode 100644 index 00000000..95730649 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-storage-bucket", + "category": 2, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-logging-bucket" + ], + "descriptiveName": "GCP Storage Bucket", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-storage-bucket by its \"name\"", + "list": true, + "listDescription": "List all gcp-storage-bucket" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_storage_bucket.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-transfer-transfer-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-transfer-transfer-job.json new file mode 100644 index 00000000..2b3bf896 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-transfer-transfer-job.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-storage-transfer-transfer-job", + "category": 2, + "potentialLinks": [ + "gcp-iam-service-account", + "gcp-pub-sub-subscription", + "gcp-pub-sub-topic", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Storage Transfer Transfer Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-storage-transfer-transfer-job by its \"name\"", + "list": true, + "listDescription": "List all gcp-storage-transfer-transfer-job" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_storage_transfer_job.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ClusterRole.md b/docs.overmind.tech/docs/sources/k8s/Types/ClusterRole.md new file mode 100644 index 00000000..cccc0bbc --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ClusterRole.md @@ -0,0 +1,17 @@ +--- +title: Cluster Role +sidebar_label: ClusterRole +--- + +A ClusterRole is a non-namespaced Kubernetes RBAC resource that groups together one or more policy rules, defining which verbs (such as `get`, `list`, `create`, `delete`) are allowed on which resources across the entire cluster. Because it is cluster-scoped, the permissions it grants apply to all namespaces. It can be referenced by a `RoleBinding` (to limit its scope to a single namespace) or by a `ClusterRoleBinding` (to apply it cluster-wide) and is commonly used to grant system-level or cross-namespace permissions to users, service accounts or other principals. +For full details, see the official Kubernetes documentation: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#clusterrole + +**Terrafrom Mappings:** + +- `kubernetes_cluster_role_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Cluster Role by name +- `LIST`: List all Cluster Roles +- `SEARCH`: Search for a Cluster Role using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ClusterRoleBinding.md b/docs.overmind.tech/docs/sources/k8s/Types/ClusterRoleBinding.md new file mode 100644 index 00000000..946b9239 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ClusterRoleBinding.md @@ -0,0 +1,28 @@ +--- +title: Cluster Role Binding +sidebar_label: ClusterRoleBinding +--- + +A ClusterRoleBinding grants the permissions defined in a `ClusterRole` to one or more subjects (users, groups, or ServiceAccounts) across the entire Kubernetes cluster. Whereas a `RoleBinding` is namespace-scoped, a ClusterRoleBinding has cluster-wide effect, making it a critical component of RBAC configuration. +For further details, see the Kubernetes documentation: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding + +**Terrafrom Mappings:** + +- `kubernetes_cluster_role_binding_v1.metadata[0].name` +- `kubernetes_cluster_role_binding.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Cluster Role Binding by name +- `LIST`: List all Cluster Role Bindings +- `SEARCH`: Search for a Cluster Role Binding using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`ClusterRole`](/sources/k8s/Types/ClusterRole) + +The ClusterRoleBinding’s `roleRef` field points to the name of a `ClusterRole`. Overmind represents this relationship so you can trace which set of permissions (rules) is being granted cluster-wide. + +### [`ServiceAccount`](/sources/k8s/Types/ServiceAccount) + +If a ClusterRoleBinding contains one or more ServiceAccounts in its `subjects` array, Overmind links the binding to those ServiceAccounts, allowing you to see exactly which workload identities receive the referenced cluster-level permissions. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ConfigMap.md b/docs.overmind.tech/docs/sources/k8s/Types/ConfigMap.md new file mode 100644 index 00000000..5dd4e332 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ConfigMap.md @@ -0,0 +1,17 @@ +--- +title: Config Map +sidebar_label: ConfigMap +--- + +A ConfigMap is a Kubernetes API object used to store non-confidential configuration data in key-value pairs. It allows you to decouple environment-specific configuration from your container images so that the same image can be reused in different environments with different settings. Pods and other Kubernetes workloads can consume the data held in a ConfigMap as environment variables, command-line arguments or configuration files mounted into a volume. For an in-depth overview, see the official documentation: https://kubernetes.io/docs/concepts/configuration/configmap/ + +**Terrafrom Mappings:** + +- `kubernetes_config_map_v1.metadata[0].name` +- `kubernetes_config_map.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Config Map by name +- `LIST`: List all Config Maps +- `SEARCH`: Search for a Config Map using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/CronJob.md b/docs.overmind.tech/docs/sources/k8s/Types/CronJob.md new file mode 100644 index 00000000..7dc0e2d2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/CronJob.md @@ -0,0 +1,17 @@ +--- +title: Cron Job +sidebar_label: CronJob +--- + +A Kubernetes **CronJob** is a higher-level controller responsible for running a Job object on a repeating schedule, expressed in standard _cron_ syntax. It is typically used for routine, time-based tasks such as database backups, report generation, and regular housekeeping activities inside a cluster. The controller automatically creates the underlying Job at the scheduled time, monitors its execution and, depending on the configuration, retains or cleans up finished Jobs and their Pods. For a full description of the resource’s behaviour and available fields, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/ + +**Terrafrom Mappings:** + +- `kubernetes_cron_job_v1.metadata[0].name` +- `kubernetes_cron_job.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Cron Job by name +- `LIST`: List all Cron Jobs +- `SEARCH`: Search for a Cron Job using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/DaemonSet.md b/docs.overmind.tech/docs/sources/k8s/Types/DaemonSet.md new file mode 100644 index 00000000..b04661a4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/DaemonSet.md @@ -0,0 +1,18 @@ +--- +title: Daemon Set +sidebar_label: DaemonSet +--- + +A Kubernetes **DaemonSet** ensures that a copy of a specified Pod is running on every (or a selected subset of) node(s) in the cluster. It is commonly used for cluster-wide services such as log collectors, monitoring agents, or network proxies that must be present on each node. When nodes are added to the cluster, the DaemonSet automatically schedules the Pod on the new nodes; when nodes are removed, the Pods are garbage-collected. +For a full description, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ + +**Terrafrom Mappings:** + +- `kubernetes_daemon_set_v1.metadata[0].name` +- `kubernetes_daemonset.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Daemon Set by name +- `LIST`: List all Daemon Sets +- `SEARCH`: Search for a Daemon Set using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Deployment.md b/docs.overmind.tech/docs/sources/k8s/Types/Deployment.md new file mode 100644 index 00000000..a11e2ab6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Deployment.md @@ -0,0 +1,24 @@ +--- +title: Deployment +sidebar_label: Deployment +--- + +A Deployment is a higher-level Kubernetes workload resource that declaratively manages a set of identical Pods by creating and maintaining the appropriate number of ReplicaSets. With a Deployment you describe the desired state—such as how many replicas should be running or which Pod template to use—and the Kubernetes control plane continually works to bring the actual state in line with that specification. Deployments support rolling updates, rollbacks, and pausing/resuming of updates, making them the most common mechanism for managing stateless applications on Kubernetes clusters. +For the complete specification see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ + +**Terrafrom Mappings:** + +- `kubernetes_deployment_v1.metadata[0].name` +- `kubernetes_deployment.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Deployment by name +- `LIST`: List all Deployments +- `SEARCH`: Search for a Deployment using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`ReplicaSet`](/sources/k8s/Types/ReplicaSet) + +Each Deployment automatically creates and owns one or more ReplicaSets. The ReplicaSet is responsible for keeping the specified number of Pod replicas running, while the Deployment supervises the ReplicaSets, deciding when to create new ones or scale them to facilitate updates or rollbacks. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md b/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md new file mode 100644 index 00000000..c8322f36 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md @@ -0,0 +1,36 @@ +--- +title: Endpoint Slice +sidebar_label: EndpointSlice +--- + +EndpointSlices provide a scalable and extensible way of tracking network endpoints that back a Kubernetes Service. Each slice contains a list of IP addresses and ports together with optional topology information such as the Node on which each endpoint is running. EndpointSlices replace the legacy Endpoints object for large clusters and are automatically created and managed by the control plane when a Service is defined. +For full details see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/ + +**Terrafrom Mappings:** + +- `kubernetes_endpoints_slice_v1.metadata[0].name` +- `kubernetes_endpoints_slice.metadata[0].name` + +## Supported Methods + +- `GET`: Get a EndpointSlice by name +- `LIST`: List all EndpointSlices +- `SEARCH`: Search for a EndpointSlice using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Node`](/sources/k8s/Types/Node) + +Each endpoint within an EndpointSlice may include a `nodeName` or topology label indicating the Node that hosts the backing Pod. Overmind links the slice to those Nodes so you can see which machines will receive traffic for the Service. + +### [`Pod`](/sources/k8s/Types/Pod) + +Endpoints usually correspond to Pod IPs. By linking EndpointSlices to the underlying Pods, Overmind allows you to trace from a Service to the exact workloads that will handle requests. + +### [`dns`](/sources/stdlib/Types/dns) + +When Kubernetes populates cluster DNS (e.g. `my-service.my-namespace.svc.cluster.local`) it ultimately resolves to the addresses listed in the Service’s EndpointSlices. Linking to DNS records shows how a name queried by applications maps to concrete endpoints. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +EndpointSlices store one or more IPv4/IPv6 addresses for each endpoint. These addresses are linked so that you can follow a path from a Service to the raw IPs that will be contacted, helping to assess network-level reachability and risk. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Endpoints.md b/docs.overmind.tech/docs/sources/k8s/Types/Endpoints.md new file mode 100644 index 00000000..34e90580 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Endpoints.md @@ -0,0 +1,31 @@ +--- +title: Endpoints +sidebar_label: Endpoints +--- + +An Endpoint in Kubernetes represents the network locations (IP address + port) that actually implement a Service. While a Service is an abstract front-end, the corresponding Endpoints object keeps the ever-changing list of Pods that are ready to receive traffic. See the official Kubernetes documentation for full details: https://kubernetes.io/docs/concepts/services-networking/service/#endpoints + +**Terrafrom Mappings:** + +- `kubernetes_endpoints.metadata[0].name` +- `kubernetes_endpoints_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Endpoints by name +- `LIST`: List all Endpointss +- `SEARCH`: Search for a Endpoints using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Node`](/sources/k8s/Types/Node) + +Each endpoint address can include a `nodeName` field indicating the Node on which the backing Pod is running. Overmind therefore links the Endpoints object to the Node(s) that currently host its backing Pods, helping you understand on which worker machines traffic will land. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Every endpoint entry exposes an IP address. Overmind extracts these IPs and links them, allowing you to trace the path from the abstract Service through the Endpoint to the concrete network address that will receive traffic. + +### [`Pod`](/sources/k8s/Types/Pod) + +Endpoint addresses typically contain a `targetRef` that points to the Pod providing the Service. Overmind links the Endpoints object to these Pods so you can quickly inspect the health, labels, and configuration of the workloads currently registered behind the Service. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/HorizontalPodAutoscaler.md b/docs.overmind.tech/docs/sources/k8s/Types/HorizontalPodAutoscaler.md new file mode 100644 index 00000000..393d4521 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/HorizontalPodAutoscaler.md @@ -0,0 +1,16 @@ +--- +title: Horizontal Pod Autoscaler +sidebar_label: HorizontalPodAutoscaler +--- + +The Horizontal Pod Autoscaler (HPA) is a native Kubernetes controller that automatically increases or decreases the number of running Pods in a Deployment, ReplicaSet, StatefulSet, or other scalable resource so that observed resource consumption stays close to a user-defined target. It polls the Kubernetes Metrics Server (or a custom/external metrics API) at a regular interval, compares CPU, memory, or arbitrary custom metrics against the specified thresholds, and then adjusts the `spec.replicas` field of the target workload accordingly. This enables applications to meet fluctuating demand without manual intervention or unnecessary over-provisioning, while still preventing sudden traffic spikes from overwhelming the cluster. You can read the full upstream specification in the official Kubernetes documentation: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/. + +**Terrafrom Mappings:** + +- `kubernetes_horizontal_pod_autoscaler_v2.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Horizontal Pod Autoscaler by name +- `LIST`: List all Horizontal Pod Autoscalers +- `SEARCH`: Search for a Horizontal Pod Autoscaler using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Ingress.md b/docs.overmind.tech/docs/sources/k8s/Types/Ingress.md new file mode 100644 index 00000000..892c1746 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Ingress.md @@ -0,0 +1,27 @@ +--- +title: Ingress +sidebar_label: Ingress +--- + +An Ingress is a Kubernetes resource that manages external access to services within a cluster, typically HTTP and HTTPS traffic. It defines a set of routing rules that map incoming requests (based on hostnames and URL paths) to backend `Service` resources. By centralising traffic management, it allows fine-grained control over features such as virtual hosting, TLS termination and path-based routing without requiring each service to expose its own `Service` of type `LoadBalancer` or `NodePort`. +Official documentation: https://kubernetes.io/docs/concepts/services-networking/ingress/ + +**Terrafrom Mappings:** + +- `kubernetes_ingress_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get an Ingress by name +- `LIST`: List all Ingresses +- `SEARCH`: Search for an Ingress using the `ListOptions` JSON format, e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Service`](/sources/k8s/Types/Service) + +An Ingress routes external traffic to one or more backend `Service` objects. Each rule in the Ingress specification references a service name and port; therefore, Overmind links an Ingress to the `Service`(s) it targets so that you can trace how requests reach your application. + +### [`dns`](/sources/stdlib/Types/dns) + +The hostnames declared in an Ingress must be resolvable via DNS so that clients can reach the cluster’s ingress point. Overmind links these hostnames to their corresponding DNS records (A, AAAA or CNAME) to show whether the necessary records exist and to surface any misconfigurations. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Job.md b/docs.overmind.tech/docs/sources/k8s/Types/Job.md new file mode 100644 index 00000000..6917f44b --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Job.md @@ -0,0 +1,23 @@ +--- +title: Job +sidebar_label: Job +--- + +A Kubernetes Job is a controller that runs one-off or batch tasks to completion. It creates one or more Pods and tracks their execution until the specified number have finished successfully. Jobs are ideal for database migrations, data processing, or any workload that needs to run to completion rather than persist indefinitely. A Job retries failed Pods according to its back-off policy and is marked as complete once all Pods exit successfully. For more details, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/job/ + +**Terrafrom Mappings:** + +- `kubernetes_job.metadata[0].name` +- `kubernetes_job_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Job by name +- `LIST`: List all Jobs +- `SEARCH`: Search for a Job using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A Job spawns one or more Pods to run its workload; each Pod created by the Job is linked back to it via the Job’s `ownerReferences` metadata. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/LimitRange.md b/docs.overmind.tech/docs/sources/k8s/Types/LimitRange.md new file mode 100644 index 00000000..0569cf33 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/LimitRange.md @@ -0,0 +1,17 @@ +--- +title: Limit Range +sidebar_label: LimitRange +--- + +A Kubernetes LimitRange is a namespace-level policy object that defines default, minimum, and maximum compute-resource constraints (such as CPU, memory, and ephemeral storage) that apply to Pods or individual Containers created in that namespace. By enforcing these boundaries, a LimitRange prevents a single workload from monopolising cluster resources and ensures that every workload has sensible defaults if the user omits explicit resource requests or limits. See the official Kubernetes documentation for full details: https://kubernetes.io/docs/concepts/policy/limit-range/ + +**Terrafrom Mappings:** + +- `kubernetes_limit_range_v1.metadata[0].name` +- `kubernetes_limit_range.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Limit Range by name +- `LIST`: List all Limit Ranges +- `SEARCH`: Search for a Limit Range using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/NetworkPolicy.md b/docs.overmind.tech/docs/sources/k8s/Types/NetworkPolicy.md new file mode 100644 index 00000000..d8270b02 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/NetworkPolicy.md @@ -0,0 +1,24 @@ +--- +title: Network Policy +sidebar_label: NetworkPolicy +--- + +A Kubernetes **NetworkPolicy** is a namespaced resource that controls how groups of Pods are allowed to communicate with each other and with other network endpoints. By defining ingress and/or egress rules that match Pods via label selectors, it provides fine-grained, declarative network segmentation inside the cluster, helping operators restrict unintended traffic and harden workloads. If no NetworkPolicy targets a Pod, that Pod is non-isolated and can both send and receive traffic to and from any source. +Official documentation: https://kubernetes.io/docs/concepts/services-networking/network-policies/ + +**Terrafrom Mappings:** + +- `kubernetes_network_policy.metadata[0].name` +- `kubernetes_network_policy_v1.metadata[0].name` + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A NetworkPolicy selects one or more Pods (in the same namespace) through `podSelector` rules; therefore, each referenced Pod can be linked to the NetworkPolicy that governs its allowed ingress and egress traffic. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Node.md b/docs.overmind.tech/docs/sources/k8s/Types/Node.md new file mode 100644 index 00000000..0e85a370 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Node.md @@ -0,0 +1,30 @@ +--- +title: Node +sidebar_label: Node +--- + +A Kubernetes **Node** is a worker machine (virtual or physical) that runs the Pods making up a cluster’s workloads. Each Node contains the services necessary to run containers, including the container runtime, kubelet and kube-proxy, and is managed by the Kubernetes control plane. For more details see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/architecture/nodes/ + +**Terrafrom Mappings:** + +- `kubernetes_node_taint.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Node by name +- `LIST`: List all Nodes +- `SEARCH`: Search for a Node using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +A Node is discoverable in the cluster via its DNS entry. Overmind links the Node to its corresponding DNS record(s) so you can trace how applications or services resolve to this worker machine. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Every Node advertises one or more internal and external IP addresses. Overmind establishes a link between the Node resource and these IP objects to surface network reachability or exposure risks. + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +When Kubernetes is running on AWS, Nodes (EC2 instances) may have EBS volumes attached to provide persistent storage for Pods. Overmind links the Node to the `ec2-volume` resources it mounts, allowing you to evaluate storage-related blast radius or compliance concerns. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolume.md b/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolume.md new file mode 100644 index 00000000..e79d6569 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolume.md @@ -0,0 +1,32 @@ +--- +title: Persistent Volume +sidebar_label: PersistentVolume +--- + +A Kubernetes PersistentVolume (PV) is a cluster-wide object that represents a piece of storage that has been provisioned either statically by an administrator or dynamically via a StorageClass. Unlike ephemeral volumes that are tied to the lifetime of a Pod, a PV exists independently and can outlive any consumer Pods, enabling stateful workloads to retain data across rescheduling or restarts. Each PV encapsulates details such as capacity, access modes, reclaim policy and the specifics of the underlying storage medium (for example, AWS EBS, NFS, or a CSI-provisioned backend). +Official documentation: https://kubernetes.io/docs/concepts/storage/persistent-volumes/ + +**Terrafrom Mappings:** + +- `kubernetes_persistent_volume.metadata[0].name` +- `kubernetes_persistent_volume_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a PersistentVolume by name +- `LIST`: List all PersistentVolumes +- `SEARCH`: Search for a PersistentVolume using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +A PersistentVolume whose `spec.awsElasticBlockStore` (or CSI driver) references an AWS EBS disk ultimately maps to an EC2 volume. Overmind links the PV to the underlying `ec2-volume` so you can assess risks such as deletion protection, encryption status or capacity limits of the actual block device. + +### [`efs-access-point`](/sources/aws/Types/efs-access-point) + +When a PV is backed by Amazon EFS via the EFS CSI driver, it mounts the file system through a specific EFS Access Point. Linking the PV to the corresponding `efs-access-point` lets you trace permissions, throughput and network configurations that could affect the workload’s storage availability. + +### [`StorageClass`](/sources/k8s/Types/StorageClass) + +Most dynamically provisioned PVs include a `storageClassName` field that references the StorageClass used to create them. By linking to the `StorageClass`, Overmind shows the provisioning parameters, reclaim policy and allowed topologies that govern how this PV was created and how it behaves when released. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolumeClaim.md b/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolumeClaim.md new file mode 100644 index 00000000..c155e143 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolumeClaim.md @@ -0,0 +1,24 @@ +--- +title: Persistent Volume Claim +sidebar_label: PersistentVolumeClaim +--- + +A PersistentVolumeClaim (PVC) in Kubernetes is a user-defined request for storage. Applications declare the amount of space, access mode and other requirements they need through a PVC, and Kubernetes finds (or waits for) a matching PersistentVolume (PV) to satisfy that request. Once bound, the PVC provides a stable, pod-agnostic handle for the underlying storage, meaning workloads can be rescheduled across nodes without losing data. +For a full explanation see the Kubernetes documentation: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + +**Terrafrom Mappings:** + +- `kubernetes_persistent_volume_claim.metadata[0].name` +- `kubernetes_persistent_volume_claim_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a PersistentVolumeClaim by name +- `LIST`: List all PersistentVolumeClaims +- `SEARCH`: Search for a PersistentVolumeClaim using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`PersistentVolume`](/sources/k8s/Types/PersistentVolume) + +A PVC is bound to a PersistentVolume that satisfies its storage class, capacity and access-mode requirements. Overmind records this binding so that from a PVC you can quickly navigate to the backing PV and assess its characteristics and any associated risks. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Pod.md b/docs.overmind.tech/docs/sources/k8s/Types/Pod.md new file mode 100644 index 00000000..f0945bf7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Pod.md @@ -0,0 +1,51 @@ +--- +title: Pod +sidebar_label: Pod +--- + +A Kubernetes Pod is the smallest deployable unit in the Kubernetes object model. It represents one or more containers that share storage, network and a specification for how to run the containers. Pods are ephemeral and are usually created and managed by higher-level controllers such as Deployments or StatefulSets. See the official Kubernetes documentation for full details: https://kubernetes.io/docs/concepts/workloads/pods/ + +**Terrafrom Mappings:** + +- `kubernetes_pod.metadata[0].name` +- `kubernetes_pod_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Pod by name +- `LIST`: List all Pods +- `SEARCH`: Search for a Pod using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`ConfigMap`](/sources/k8s/Types/ConfigMap) + +Pods can consume ConfigMaps as environment variables or mount them as files, allowing configuration data to be injected without rebuilding container images. + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +When a Pod mounts a PersistentVolume backed by an AWS Elastic Block Store (EBS) volume, that underlying storage appears here as an `ec2-volume` link, connecting the workload to the physical disk resource in AWS. + +### [`dns`](/sources/stdlib/Types/dns) + +Each Pod receives an internal DNS entry (`..pod.cluster.local`) and may resolve or be resolved by other services; Overmind records this relationship so you can trace DNS dependencies. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +At runtime every Pod is assigned an IP address. This link surfaces the relationship between the Kubernetes object and the network IP resource managed by the underlying cloud networking layer. + +### [`PersistentVolumeClaim`](/sources/k8s/Types/PersistentVolumeClaim) + +Pods declare one or more PersistentVolumeClaims in their `volumes` section to obtain persistent storage. The link shows which claims are attached to the Pod. + +### [`PriorityClass`](/sources/k8s/Types/PriorityClass) + +A Pod may specify a `priorityClassName`; the associated PriorityClass influences scheduling order and pre-emption behaviour. This link ties the Pod to its scheduling priority. + +### [`Secret`](/sources/k8s/Types/Secret) + +Secrets can be mounted as files or injected as environment variables into a Pod, for example to provide credentials or TLS keys. This link identifies every Secret the Pod references. + +### [`ServiceAccount`](/sources/k8s/Types/ServiceAccount) + +Each Pod runs under a ServiceAccount that defines its Kubernetes API permissions and, in many cases, its cloud IAM identity. The link shows the ServiceAccount used by the Pod. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/PodDisruptionBudget.md b/docs.overmind.tech/docs/sources/k8s/Types/PodDisruptionBudget.md new file mode 100644 index 00000000..350f3102 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/PodDisruptionBudget.md @@ -0,0 +1,23 @@ +--- +title: Pod Disruption Budget +sidebar_label: PodDisruptionBudget +--- + +A PodDisruptionBudget (PDB) is a Kubernetes policy object that limits the number of pods of a replicated application that can be unavailable during voluntary disruptions such as a node drain, cluster upgrade, or a user-initiated rolling update. By defining either a `minAvailable` or `maxUnavailable` threshold, it helps you maintain a desired level of service availability while still allowing the platform to carry out maintenance tasks. +See the official documentation for full details: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/ + +**Terrafrom Mappings:** + +- `kubernetes_pod_disruption_budget_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a PodDisruptionBudget by name +- `LIST`: List all PodDisruptionBudgets +- `SEARCH`: Search for a PodDisruptionBudget using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A PodDisruptionBudget references pods via a label selector defined in `spec.selector`. Any pod whose labels match this selector is governed by the PDB, meaning it counts towards the availability calculations and is protected from eviction when the defined disruption limits would be exceeded. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/PriorityClass.md b/docs.overmind.tech/docs/sources/k8s/Types/PriorityClass.md new file mode 100644 index 00000000..51c473e4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/PriorityClass.md @@ -0,0 +1,17 @@ +--- +title: Priority Class +sidebar_label: PriorityClass +--- + +A Kubernetes `PriorityClass` is a cluster-wide, non-namespaced resource that defines the relative importance of Pods. Each PriorityClass carries an integer value; the higher the value, the earlier the scheduler will try to place Pods that reference it. PriorityClasses are also used during pre-emption: when the cluster is under resource pressure, Pods with lower priority may be evicted in favour of higher-priority Pods. For full details, refer to the official Kubernetes documentation: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass + +**Terrafrom Mappings:** + +- `kubernetes_priority_class_v1.metadata[0].name` +- `kubernetes_priority_class.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Priority Class by name +- `LIST`: List all Priority Classs +- `SEARCH`: Search for a Priority Class using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ReplicaSet.md b/docs.overmind.tech/docs/sources/k8s/Types/ReplicaSet.md new file mode 100644 index 00000000..d5723a62 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ReplicaSet.md @@ -0,0 +1,19 @@ +--- +title: Replica Set +sidebar_label: ReplicaSet +--- + +A ReplicaSet is a Kubernetes controller whose purpose is to maintain a stable set of identical Pods running at any given time. By continuously watching the cluster state, it ensures that the desired number of Pod replicas are present: if one is deleted or becomes unhealthy, the ReplicaSet will automatically create a replacement. ReplicaSets are most commonly created implicitly by Deployments, but they can also be defined directly. +For full details, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ + +## Supported Methods + +- `GET`: Get a ReplicaSet by name +- `LIST`: List all ReplicaSets +- `SEARCH`: Search for a ReplicaSet using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A ReplicaSet owns and manages a collection of Pods that match its selector. Each linked Pod represents one replica maintained by the ReplicaSet; scaling or health-checking operations performed by the ReplicaSet directly affect these Pods. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ReplicationController.md b/docs.overmind.tech/docs/sources/k8s/Types/ReplicationController.md new file mode 100644 index 00000000..3780e9de --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ReplicationController.md @@ -0,0 +1,23 @@ +--- +title: Replication Controller +sidebar_label: ReplicationController +--- + +A ReplicationController is a legacy Kubernetes workload controller whose job is to ensure that a specified number of pod replicas are running at any one time. If a pod crashes or is deleted, the ReplicationController creates a replacement; if too many exist, it deletes the excess. Although superseded by ReplicaSets and Deployments, ReplicationControllers are still respected by the Kubernetes API and may be encountered in older manifests. Further information can be found in the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/ + +**Terrafrom Mappings:** + +- `kubernetes_replication_controller.metadata[0].name` +- `kubernetes_replication_controller_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a ReplicationController by name +- `LIST`: List all ReplicationControllers +- `SEARCH`: Search for a ReplicationController using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A ReplicationController manages the lifecycle of a homogeneous set of Pods defined by its `spec.template`. Overmind links a ReplicationController to each Pod it owns via the `ownerReference`, enabling you to trace from controller to running workload (and vice-versa) when assessing deployment risk. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ResourceQuota.md b/docs.overmind.tech/docs/sources/k8s/Types/ResourceQuota.md new file mode 100644 index 00000000..5388ec70 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ResourceQuota.md @@ -0,0 +1,18 @@ +--- +title: Resource Quota +sidebar_label: ResourceQuota +--- + +A Kubernetes **ResourceQuota** object allows cluster administrators to limit the aggregate consumption of compute resources (such as CPU and memory), storage, and object counts (Pods, Services, PersistentVolumeClaims, etc.) within a namespace. By defining upper bounds, a ResourceQuota helps prevent any single team or workload from exhausting shared cluster capacity, and encourages fair usage across tenants. When a namespace has one or more quotas in place, resources are checked at creation or update time; if the requested amount would exceed the quota the operation is rejected. +Official documentation: https://kubernetes.io/docs/concepts/policy/resource-quotas/ + +**Terrafrom Mappings:** + +- `kubernetes_resource_quota_v1.metadata[0].name` +- `kubernetes_resource_quota.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Resource Quota by name +- `LIST`: List all Resource Quotas +- `SEARCH`: Search for a Resource Quota using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Role.md b/docs.overmind.tech/docs/sources/k8s/Types/Role.md new file mode 100644 index 00000000..38f16e56 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Role.md @@ -0,0 +1,18 @@ +--- +title: Role +sidebar_label: Role +--- + +A Kubernetes Role is an RBAC (Role-Based Access Control) resource that defines a set of permissions, expressed as rules, that apply within a single namespace. By binding a Role to a Subject (user, group, or service account) you control which verbs (get, list, create, delete, etc.) can be performed on which API resources inside that namespace. Roles are therefore central to enforcing the principle of least privilege in cluster security. +See the official Kubernetes documentation for full details: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole + +**Terrafrom Mappings:** + +- `kubernetes_role_v1.metadata[0].name` +- `kubernetes_role.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Role by name +- `LIST`: List all Roles +- `SEARCH`: Search for a Role using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/RoleBinding.md b/docs.overmind.tech/docs/sources/k8s/Types/RoleBinding.md new file mode 100644 index 00000000..428b1dcf --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/RoleBinding.md @@ -0,0 +1,31 @@ +--- +title: Role Binding +sidebar_label: RoleBinding +--- + +A Kubernetes **RoleBinding** grants the permissions defined in a Role (or ClusterRole) to a set of subjects—users, groups or service accounts—within a single namespace. It is a cornerstone object in Kubernetes RBAC, controlling who can perform which actions on namespaced resources. See the official Kubernetes documentation for full details: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding + +**Terrafrom Mappings:** + +- `kubernetes_role_binding.metadata[0].name` +- `kubernetes_role_binding_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a RoleBinding by name +- `LIST`: List all RoleBindings +- `SEARCH`: Search for a RoleBinding using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Role`](/sources/k8s/Types/Role) + +The RoleBinding points to a Role via the `roleRef` field. This link lets Overmind trace which set of rules (verbs, resources, API groups) will be granted when the RoleBinding is applied. + +### [`ClusterRole`](/sources/k8s/Types/ClusterRole) + +Although scoped to a namespace, a RoleBinding can reference a ClusterRole instead of a Role. Overmind links the two so you can see when cluster-wide permission sets are being delegated into a namespace. + +### [`ServiceAccount`](/sources/k8s/Types/ServiceAccount) + +Service accounts commonly appear in the `subjects` list of a RoleBinding. Linking these enables Overmind to reveal which workloads (pods using the service account) will inherit the referenced permissions. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Secret.md b/docs.overmind.tech/docs/sources/k8s/Types/Secret.md new file mode 100644 index 00000000..1c504e2f --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Secret.md @@ -0,0 +1,17 @@ +--- +title: Secret +sidebar_label: Secret +--- + +A Kubernetes Secret is an object that holds a small amount of sensitive data—such as passwords, tokens, or keys—so that it can be used by Pods without being written to image or configuration files. Storing confidential information in a Secret allows you to keep it separate from application code and to control how and when it is exposed to the running workload. For a detailed overview, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/configuration/secret/ + +**Terrafrom Mappings:** + +- `kubernetes_secret_v1.metadata[0].name` +- `kubernetes_secret.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Secret by name +- `LIST`: List all Secrets +- `SEARCH`: Search for a Secret using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Service.md b/docs.overmind.tech/docs/sources/k8s/Types/Service.md new file mode 100644 index 00000000..039fe307 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Service.md @@ -0,0 +1,32 @@ +--- +title: Service +sidebar_label: Service +--- + +A Kubernetes Service is an abstract resource that defines a logical set of Pods and the policy by which they can be accessed. It provides a stable virtual IP (ClusterIP), DNS entry and, depending on the type, can expose workloads internally within the cluster or externally to the Internet through NodePorts or cloud load-balancers. Services decouple network identity and discovery from the underlying Pods, allowing them to scale up, down, or be replaced without changing the connection endpoint. +For full details see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/services-networking/service/ + +**Terrafrom Mappings:** + +- `kubernetes_service.metadata[0].name` +- `kubernetes_service_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Service by name +- `LIST`: List all Services +- `SEARCH`: Search for a Service using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A Service selects one or more Pods via label selectors and forwards traffic to them. Overmind links Services to the Pods that currently match their selector so you can see which workloads will receive traffic. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Each Service is assigned one or more IP addresses (ClusterIP, ExternalIP, LoadBalancer IP). Overmind creates links to these IP resources to show the concrete network endpoints associated with the Service. + +### [`dns`](/sources/stdlib/Types/dns) + +Kubernetes automatically registers DNS records for every Service (e.g., `my-service.my-namespace.svc.cluster.local`). Overmind links Services to their corresponding DNS entries so you can trace name resolution to the backing workloads. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ServiceAccount.md b/docs.overmind.tech/docs/sources/k8s/Types/ServiceAccount.md new file mode 100644 index 00000000..5e2ed01b --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ServiceAccount.md @@ -0,0 +1,23 @@ +--- +title: Service Account +sidebar_label: ServiceAccount +--- + +A ServiceAccount is a Kubernetes resource that provides an identity to processes running inside Pods, allowing them to authenticate to the Kubernetes API and other services with the minimum privileges required. Each ServiceAccount can be linked to one or more Secrets that store its bearer token or image-pull credentials, and these Secrets are automatically mounted into Pods that specify the ServiceAccount. Further information can be found in the official Kubernetes documentation: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/. + +**Terrafrom Mappings:** + +- `kubernetes_service_account.metadata[0].name` +- `kubernetes_service_account_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a ServiceAccount by name +- `LIST`: List all ServiceAccounts +- `SEARCH`: Search for a ServiceAccount using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Secret`](/sources/k8s/Types/Secret) + +A ServiceAccount is associated with Secrets that hold its authentication token or are referenced in `imagePullSecrets`. These Secrets determine how Pods using the ServiceAccount authenticate to the cluster or to external registries, making them critical for understanding access scopes and potential risk. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/StatefulSet.md b/docs.overmind.tech/docs/sources/k8s/Types/StatefulSet.md new file mode 100644 index 00000000..e7dd27b9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/StatefulSet.md @@ -0,0 +1,18 @@ +--- +title: Stateful Set +sidebar_label: StatefulSet +--- + +A StatefulSet is a Kubernetes workload controller that manages the deployment and scaling of a set of Pods, while guaranteeing the ordering and uniqueness of those Pods. Unlike Deployments, which are optimised for stateless services, StatefulSets are designed for applications that require stable network identities, stable persistent storage and ordered, graceful deployment and scaling. Typical use-cases include databases, distributed filesystems and clustered applications where each replica must be uniquely addressable. +For full details, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ + +**Terrafrom Mappings:** + +- `kubernetes_stateful_set_v1.metadata[0].name` +- `kubernetes_stateful_set.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Stateful Set by name +- `LIST`: List all Stateful Sets +- `SEARCH`: Search for a Stateful Set using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/StorageClass.md b/docs.overmind.tech/docs/sources/k8s/Types/StorageClass.md new file mode 100644 index 00000000..82202e32 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/StorageClass.md @@ -0,0 +1,18 @@ +--- +title: Storage Class +sidebar_label: StorageClass +--- + +A StorageClass is a cluster-wide Kubernetes resource that defines a “class” or tier of persistent storage that can be requested by workloads. Each StorageClass couples a provisioner (for example an AWS EBS driver, a CSI plug-in, or a Ceph back-end) with a set of parameters such as performance characteristics, encryption settings, reclaim policy, and mount options. When a user creates a PersistentVolumeClaim that references a particular `storageClassName`, Kubernetes dynamically provisions a matching PersistentVolume according to the rules in the StorageClass and binds it to the claim. This abstraction lets platform teams expose multiple quality-of-service levels while shielding application teams from underlying infrastructure details. +Official documentation: https://kubernetes.io/docs/concepts/storage/storage-classes/ + +**Terrafrom Mappings:** + +- `kubernetes_storage_class.metadata[0].name` +- `kubernetes_storage_class_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Storage Class by name +- `LIST`: List all Storage Classs +- `SEARCH`: Search for a Storage Class using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/VolumeAttachment.md b/docs.overmind.tech/docs/sources/k8s/Types/VolumeAttachment.md new file mode 100644 index 00000000..62d7d4c3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/VolumeAttachment.md @@ -0,0 +1,23 @@ +--- +title: Volume Attachment +sidebar_label: VolumeAttachment +--- + +A Kubernetes `VolumeAttachment` represents the intent to attach (or detach) a PersistentVolume to a specific Node. It is created and managed automatically by the external CSI attacher or the in-tree volume controller whenever a Pod that uses a PersistentVolume is scheduled. Kubernetes will not make the volume available to the Pod until the corresponding `VolumeAttachment` reports that the attach operation has completed successfully. +For full details see the official Kubernetes documentation: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume-attachment-v1/ + +## Supported Methods + +- `GET`: Get a VolumeAttachment by name +- `LIST`: List all VolumeAttachments +- `SEARCH`: Search for a VolumeAttachment using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`PersistentVolume`](/sources/k8s/Types/PersistentVolume) + +`VolumeAttachment.spec.source.persistentVolumeName` holds the name of the PersistentVolume to be attached. Overmind links the `VolumeAttachment` to this `PersistentVolume` so you can trace which physical storage device is being mounted on which node. + +### [`Node`](/sources/k8s/Types/Node) + +`VolumeAttachment.spec.nodeName` identifies the Node where the volume should be attached. Linking `VolumeAttachment` to the `Node` lets you understand which worker machine will host the volume and helps assess the impact of node-specific storage operations. diff --git a/docs.overmind.tech/docs/sources/k8s/_category_.json b/docs.overmind.tech/docs/sources/k8s/_category_.json new file mode 100644 index 00000000..dd873fc2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Kubernetes", + "position": 2, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to integrate your k8s cluster." + } +} diff --git a/docs.overmind.tech/docs/sources/k8s/account_settings.png b/docs.overmind.tech/docs/sources/k8s/account_settings.png new file mode 100644 index 00000000..4e50b499 Binary files /dev/null and b/docs.overmind.tech/docs/sources/k8s/account_settings.png differ diff --git a/docs.overmind.tech/docs/sources/k8s/api_key.png b/docs.overmind.tech/docs/sources/k8s/api_key.png new file mode 100644 index 00000000..0d72300e Binary files /dev/null and b/docs.overmind.tech/docs/sources/k8s/api_key.png differ diff --git a/docs.overmind.tech/docs/sources/k8s/configuration.md b/docs.overmind.tech/docs/sources/k8s/configuration.md new file mode 100644 index 00000000..7df113af --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/configuration.md @@ -0,0 +1,152 @@ +--- +title: Kubernetes Setup +sidebar_position: 1 +--- + +## Prerequisites + +- Kubernetes 1.16+ +- Helm 3.x +- An Overmind API key with `request:receive` scope + +## Installation + +Create an API Key with `request:receive` scope in Overmind under Account settings > API Keys + +![account settings](account_settings.png) +![api key](api_key.png) + +Install the source into your Kubernetes cluster using Helm: + +```sh +helm repo add overmind https://dl.cloudsmith.io/public/overmind/tools/helm/charts +helm install overmind-kube-source overmind/overmind-kube-source \ + --set source.apiKey.value=YOUR_API_KEY \ + --set source.clusterName=my-cluster-name +``` + +## Uninstalling + +```sh +helm uninstall overmind-kube-source +``` + +## Upgrading + +```shell +helm upgrade overmind-kube-source overmind/overmind-kube-source +``` + +## Configuration + +The following table lists the configurable parameters and their default values. + +### Image Configuration + +| Parameter | Description | Default | +| ------------------ | ---------------------------------- | ------------------------------------------- | +| `image.repository` | Image repository | `ghcr.io/overmindtech/workspace/k8s-source` | +| `image.pullPolicy` | Image pull policy | `Always` | +| `image.tag` | Image tag (defaults to appVersion) | `""` | +| `imagePullSecrets` | Image pull secrets | `[]` | + +### Deployment Configuration + +| Parameter | Description | Default | +| -------------------- | -------------------------- | ------- | +| `replicaCount` | Number of replicas | `1` | +| `nameOverride` | Override chart name | `""` | +| `fullnameOverride` | Override full name | `""` | +| `podAnnotations` | Pod annotations | `{}` | +| `podSecurityContext` | Pod security context | `{}` | +| `securityContext` | Container security context | `{}` | +| `nodeSelector` | Node selector | `{}` | +| `tolerations` | Pod tolerations | `[]` | +| `affinity` | Pod affinity rules | `{}` | + +### Source Configuration + +| Parameter | Description | Default | +| ---------------------------------- | ----------------------------------------------------- | --------------------------- | +| `source.log` | Log level (info, debug, trace) | `info` | +| `source.apiKey.value` | Direct API key value (not recommended for production) | `""` | +| `source.apiKey.existingSecretName` | Name of existing secret containing API key | `""` | +| `source.app` | Overmind instance URL | `https://app.overmind.tech` | +| `source.maxParallel` | Max parallel requests | `20` | +| `source.rateLimitQPS` | K8s API rate limit QPS | `10` | +| `source.rateLimitBurst` | K8s API rate limit burst | `30` | +| `source.clusterName` | Cluster name | `""` | +| `source.honeycombApiKey` | Honeycomb API key | `""` | + +### Pod Disruption Budget Configuration + +| Parameter | Description | Default | +| ----------------------------- | ---------------------------- | ------- | +| `podDisruptionBudget.enabled` | Enable Pod Disruption Budget | `true` | + +### Example values.yaml + +```yaml +source: + apiKey: 'your-api-key' + clusterName: 'production-cluster' + log: 'debug' + maxParallel: 30 + rateLimitQPS: 20 + rateLimitBurst: 40 + +# Pod Disruption Budget is enabled by default for production protection +podDisruptionBudget: + enabled: true + +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +``` + +## API Key Management + +The chart provides two methods for managing the required Overmind API key: + +### Using an Existing Secret + +1. Create a Kubernetes secret containing your API key: + + ```sh + kubectl create secret generic overmind-api-key \ + --from-literal=API_KEY=your-api-key-here + ``` + +2. Install the chart: + + ```sh + helm install overmind-kube-source overmind/overmind-kube-source \ + --set source.apiKey.existingSecretName=overmind-api-key + ``` + +**Important Notes:** + +- The secret MUST contain a key named `API_KEY` +- The secret must exist in the same namespace as the chart +- Installation will fail if: + - The secret doesn't exist + - The secret exists but doesn't contain an `API_KEY` key + - Neither `source.apiKey.existingSecretName` nor `source.apiKey.value` is provided + +### Using Direct Value + +```sh +helm install overmind-kube-source overmind/overmind-kube-source \ + --set source.apiKey.value=YOUR_API_KEY + --set source.clusterName=my-cluster-name +``` + +**Warning:** This method stores the API key in clear text in your values file. Only use for development/testing. + +## Support + +This source will support all Kubernetes versions that are currently maintained in the kubernetes project. The list can be found [here](https://kubernetes.io/releases/) diff --git a/docs.overmind.tech/docs/sources/k8s/data/ClusterRole.json b/docs.overmind.tech/docs/sources/k8s/data/ClusterRole.json new file mode 100644 index 00000000..1392e33e --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ClusterRole.json @@ -0,0 +1,18 @@ +{ + "type": "ClusterRole", + "category": 4, + "descriptiveName": "Cluster Role", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Cluster Role by name", + "list": true, + "listDescription": "List all Cluster Roles", + "search": true, + "searchDescription": "Search for a Cluster Role using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_cluster_role_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ClusterRoleBinding.json b/docs.overmind.tech/docs/sources/k8s/data/ClusterRoleBinding.json new file mode 100644 index 00000000..e74b86e7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ClusterRoleBinding.json @@ -0,0 +1,22 @@ +{ + "type": "ClusterRoleBinding", + "category": 4, + "potentialLinks": ["ClusterRole", "ServiceAccount", "User", "Group"], + "descriptiveName": "Cluster Role Binding", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Cluster Role Binding by name", + "list": true, + "listDescription": "List all Cluster Role Bindings", + "search": true, + "searchDescription": "Search for a Cluster Role Binding using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_cluster_role_binding_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_cluster_role_binding.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ConfigMap.json b/docs.overmind.tech/docs/sources/k8s/data/ConfigMap.json new file mode 100644 index 00000000..08db112b --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ConfigMap.json @@ -0,0 +1,21 @@ +{ + "type": "ConfigMap", + "category": 7, + "descriptiveName": "Config Map", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Config Map by name", + "list": true, + "listDescription": "List all Config Maps", + "search": true, + "searchDescription": "Search for a Config Map using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_config_map_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_config_map.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/CronJob.json b/docs.overmind.tech/docs/sources/k8s/data/CronJob.json new file mode 100644 index 00000000..8780624c --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/CronJob.json @@ -0,0 +1,21 @@ +{ + "type": "CronJob", + "category": 1, + "descriptiveName": "Cron Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Cron Job by name", + "list": true, + "listDescription": "List all Cron Jobs", + "search": true, + "searchDescription": "Search for a Cron Job using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_cron_job_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_cron_job.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/DaemonSet.json b/docs.overmind.tech/docs/sources/k8s/data/DaemonSet.json new file mode 100644 index 00000000..26aa22bf --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/DaemonSet.json @@ -0,0 +1,21 @@ +{ + "type": "DaemonSet", + "category": 1, + "descriptiveName": "Daemon Set", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Daemon Set by name", + "list": true, + "listDescription": "List all Daemon Sets", + "search": true, + "searchDescription": "Search for a Daemon Set using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_daemon_set_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_daemonset.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Deployment.json b/docs.overmind.tech/docs/sources/k8s/data/Deployment.json new file mode 100644 index 00000000..d683258a --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Deployment.json @@ -0,0 +1,22 @@ +{ + "type": "Deployment", + "category": 1, + "potentialLinks": ["ReplicaSet"], + "descriptiveName": "Deployment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Deployment by name", + "list": true, + "listDescription": "List all Deployments", + "search": true, + "searchDescription": "Search for a Deployment using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_deployment_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_deployment.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json b/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json new file mode 100644 index 00000000..a8a9ef2a --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json @@ -0,0 +1,22 @@ +{ + "type": "EndpointSlice", + "category": 3, + "potentialLinks": ["Node", "Pod", "dns", "ip"], + "descriptiveName": "Endpoint Slice", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a EndpointSlice by name", + "list": true, + "listDescription": "List all EndpointSlices", + "search": true, + "searchDescription": "Search for a EndpointSlice using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_endpoints_slice_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_endpoints_slice.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Endpoints.json b/docs.overmind.tech/docs/sources/k8s/data/Endpoints.json new file mode 100644 index 00000000..0edba0b7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Endpoints.json @@ -0,0 +1,22 @@ +{ + "type": "Endpoints", + "category": 3, + "potentialLinks": ["Node", "ip", "Pod", "ExternalName", "DNS"], + "descriptiveName": "Endpoints", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Endpoints by name", + "list": true, + "listDescription": "List all Endpointss", + "search": true, + "searchDescription": "Search for a Endpoints using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_endpoints.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_endpoints_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/HorizontalPodAutoscaler.json b/docs.overmind.tech/docs/sources/k8s/data/HorizontalPodAutoscaler.json new file mode 100644 index 00000000..bd809e11 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/HorizontalPodAutoscaler.json @@ -0,0 +1,18 @@ +{ + "type": "HorizontalPodAutoscaler", + "category": 7, + "descriptiveName": "Horizontal Pod Autoscaler", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Horizontal Pod Autoscaler by name", + "list": true, + "listDescription": "List all Horizontal Pod Autoscalers", + "search": true, + "searchDescription": "Search for a Horizontal Pod Autoscaler using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_horizontal_pod_autoscaler_v2.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Ingress.json b/docs.overmind.tech/docs/sources/k8s/data/Ingress.json new file mode 100644 index 00000000..0cd1a1ab --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Ingress.json @@ -0,0 +1,19 @@ +{ + "type": "Ingress", + "category": 3, + "potentialLinks": ["Service", "IngressClass", "dns"], + "descriptiveName": "Ingress", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Ingress by name", + "list": true, + "listDescription": "List all Ingresss", + "search": true, + "searchDescription": "Search for a Ingress using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_ingress_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Job.json b/docs.overmind.tech/docs/sources/k8s/data/Job.json new file mode 100644 index 00000000..65e9cec1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Job.json @@ -0,0 +1,22 @@ +{ + "type": "Job", + "category": 1, + "potentialLinks": ["Pod"], + "descriptiveName": "Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Job by name", + "list": true, + "listDescription": "List all Jobs", + "search": true, + "searchDescription": "Search for a Job using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_job.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_job_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/LimitRange.json b/docs.overmind.tech/docs/sources/k8s/data/LimitRange.json new file mode 100644 index 00000000..2d34fa2d --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/LimitRange.json @@ -0,0 +1,21 @@ +{ + "type": "LimitRange", + "category": 7, + "descriptiveName": "Limit Range", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Limit Range by name", + "list": true, + "listDescription": "List all Limit Ranges", + "search": true, + "searchDescription": "Search for a Limit Range using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_limit_range_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_limit_range.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/NetworkPolicy.json b/docs.overmind.tech/docs/sources/k8s/data/NetworkPolicy.json new file mode 100644 index 00000000..ae7f932e --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/NetworkPolicy.json @@ -0,0 +1,14 @@ +{ + "type": "NetworkPolicy", + "category": 4, + "potentialLinks": ["Pod"], + "descriptiveName": "Network Policy", + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_network_policy.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_network_policy_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Node.json b/docs.overmind.tech/docs/sources/k8s/data/Node.json new file mode 100644 index 00000000..67da5db0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Node.json @@ -0,0 +1,19 @@ +{ + "type": "Node", + "category": 1, + "potentialLinks": ["dns", "ip", "ec2-volume"], + "descriptiveName": "Node", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Node by name", + "list": true, + "listDescription": "List all Nodes", + "search": true, + "searchDescription": "Search for a Node using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_node_taint.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/PersistentVolume.json b/docs.overmind.tech/docs/sources/k8s/data/PersistentVolume.json new file mode 100644 index 00000000..cb128430 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/PersistentVolume.json @@ -0,0 +1,22 @@ +{ + "type": "PersistentVolume", + "category": 2, + "potentialLinks": ["ec2-volume", "efs-access-point", "StorageClass"], + "descriptiveName": "Persistent Volume", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a PersistentVolume by name", + "list": true, + "listDescription": "List all PersistentVolumes", + "search": true, + "searchDescription": "Search for a PersistentVolume using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_persistent_volume.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_persistent_volume_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/PersistentVolumeClaim.json b/docs.overmind.tech/docs/sources/k8s/data/PersistentVolumeClaim.json new file mode 100644 index 00000000..4880294a --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/PersistentVolumeClaim.json @@ -0,0 +1,22 @@ +{ + "type": "PersistentVolumeClaim", + "category": 2, + "potentialLinks": ["PersistentVolume"], + "descriptiveName": "Persistent Volume Claim", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a PersistentVolumeClaim by name", + "list": true, + "listDescription": "List all PersistentVolumeClaims", + "search": true, + "searchDescription": "Search for a PersistentVolumeClaim using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_persistent_volume_claim.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_persistent_volume_claim_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Pod.json b/docs.overmind.tech/docs/sources/k8s/data/Pod.json new file mode 100644 index 00000000..26e8d232 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Pod.json @@ -0,0 +1,31 @@ +{ + "type": "Pod", + "category": 1, + "potentialLinks": [ + "ConfigMap", + "ec2-volume", + "dns", + "ip", + "PersistentVolumeClaim", + "PriorityClass", + "Secret", + "ServiceAccount" + ], + "descriptiveName": "Pod", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Pod by name", + "list": true, + "listDescription": "List all Pods", + "search": true, + "searchDescription": "Search for a Pod using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_pod.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_pod_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/PodDisruptionBudget.json b/docs.overmind.tech/docs/sources/k8s/data/PodDisruptionBudget.json new file mode 100644 index 00000000..356471b5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/PodDisruptionBudget.json @@ -0,0 +1,19 @@ +{ + "type": "PodDisruptionBudget", + "category": 7, + "potentialLinks": ["Pod"], + "descriptiveName": "Pod Disruption Budget", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a PodDisruptionBudget by name", + "list": true, + "listDescription": "List all PodDisruptionBudgets", + "search": true, + "searchDescription": "Search for a PodDisruptionBudget using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_pod_disruption_budget_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/PriorityClass.json b/docs.overmind.tech/docs/sources/k8s/data/PriorityClass.json new file mode 100644 index 00000000..bfc8f10a --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/PriorityClass.json @@ -0,0 +1,21 @@ +{ + "type": "PriorityClass", + "category": 7, + "descriptiveName": "Priority Class", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Priority Class by name", + "list": true, + "listDescription": "List all Priority Classs", + "search": true, + "searchDescription": "Search for a Priority Class using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_priority_class_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_priority_class.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ReplicaSet.json b/docs.overmind.tech/docs/sources/k8s/data/ReplicaSet.json new file mode 100644 index 00000000..4e4900b9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ReplicaSet.json @@ -0,0 +1,14 @@ +{ + "type": "ReplicaSet", + "category": 1, + "potentialLinks": ["Pod"], + "descriptiveName": "Replica Set", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a ReplicaSet by name", + "list": true, + "listDescription": "List all ReplicaSets", + "search": true, + "searchDescription": "Search for a ReplicaSet using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + } +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ReplicationController.json b/docs.overmind.tech/docs/sources/k8s/data/ReplicationController.json new file mode 100644 index 00000000..3c859acd --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ReplicationController.json @@ -0,0 +1,22 @@ +{ + "type": "ReplicationController", + "category": 1, + "potentialLinks": ["Pod"], + "descriptiveName": "Replication Controller", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a ReplicationController by name", + "list": true, + "listDescription": "List all ReplicationControllers", + "search": true, + "searchDescription": "Search for a ReplicationController using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_replication_controller.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_replication_controller_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ResourceQuota.json b/docs.overmind.tech/docs/sources/k8s/data/ResourceQuota.json new file mode 100644 index 00000000..84d3c985 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ResourceQuota.json @@ -0,0 +1,21 @@ +{ + "type": "ResourceQuota", + "category": 7, + "descriptiveName": "Resource Quota", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Resource Quota by name", + "list": true, + "listDescription": "List all Resource Quotas", + "search": true, + "searchDescription": "Search for a Resource Quota using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_resource_quota_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_resource_quota.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Role.json b/docs.overmind.tech/docs/sources/k8s/data/Role.json new file mode 100644 index 00000000..9783695b --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Role.json @@ -0,0 +1,21 @@ +{ + "type": "Role", + "category": 4, + "descriptiveName": "Role", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Role by name", + "list": true, + "listDescription": "List all Roles", + "search": true, + "searchDescription": "Search for a Role using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_role_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_role.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/RoleBinding.json b/docs.overmind.tech/docs/sources/k8s/data/RoleBinding.json new file mode 100644 index 00000000..2104a973 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/RoleBinding.json @@ -0,0 +1,22 @@ +{ + "type": "RoleBinding", + "category": 4, + "potentialLinks": ["Role", "ClusterRole", "ServiceAccount", "User", "Group"], + "descriptiveName": "Role Binding", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a RoleBinding by name", + "list": true, + "listDescription": "List all RoleBindings", + "search": true, + "searchDescription": "Search for a RoleBinding using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_role_binding.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_role_binding_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Secret.json b/docs.overmind.tech/docs/sources/k8s/data/Secret.json new file mode 100644 index 00000000..bf72898c --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Secret.json @@ -0,0 +1,21 @@ +{ + "type": "Secret", + "category": 7, + "descriptiveName": "Secret", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Secret by name", + "list": true, + "listDescription": "List all Secrets", + "search": true, + "searchDescription": "Search for a Secret using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_secret_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_secret.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Service.json b/docs.overmind.tech/docs/sources/k8s/data/Service.json new file mode 100644 index 00000000..4f22397c --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Service.json @@ -0,0 +1,22 @@ +{ + "type": "Service", + "category": 3, + "potentialLinks": ["Pod", "ip", "dns", "Endpoint"], + "descriptiveName": "Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Service by name", + "list": true, + "listDescription": "List all Services", + "search": true, + "searchDescription": "Search for a Service using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_service.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_service_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ServiceAccount.json b/docs.overmind.tech/docs/sources/k8s/data/ServiceAccount.json new file mode 100644 index 00000000..f5920a8f --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ServiceAccount.json @@ -0,0 +1,22 @@ +{ + "type": "ServiceAccount", + "category": 4, + "potentialLinks": ["Secret"], + "descriptiveName": "Service Account", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a ServiceAccount by name", + "list": true, + "listDescription": "List all ServiceAccounts", + "search": true, + "searchDescription": "Search for a ServiceAccount using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_service_account.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_service_account_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/StatefulSet.json b/docs.overmind.tech/docs/sources/k8s/data/StatefulSet.json new file mode 100644 index 00000000..b7c59ab6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/StatefulSet.json @@ -0,0 +1,21 @@ +{ + "type": "StatefulSet", + "category": 1, + "descriptiveName": "Stateful Set", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Stateful Set by name", + "list": true, + "listDescription": "List all Stateful Sets", + "search": true, + "searchDescription": "Search for a Stateful Set using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_stateful_set_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_stateful_set.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/StorageClass.json b/docs.overmind.tech/docs/sources/k8s/data/StorageClass.json new file mode 100644 index 00000000..c1a26a10 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/StorageClass.json @@ -0,0 +1,21 @@ +{ + "type": "StorageClass", + "category": 2, + "descriptiveName": "Storage Class", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Storage Class by name", + "list": true, + "listDescription": "List all Storage Classs", + "search": true, + "searchDescription": "Search for a Storage Class using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_storage_class.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_storage_class_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/VolumeAttachment.json b/docs.overmind.tech/docs/sources/k8s/data/VolumeAttachment.json new file mode 100644 index 00000000..7d1a59b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/VolumeAttachment.json @@ -0,0 +1,14 @@ +{ + "type": "VolumeAttachment", + "category": 2, + "potentialLinks": ["PersistentVolume", "Node"], + "descriptiveName": "Volume Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a VolumeAttachment by name", + "list": true, + "listDescription": "List all VolumeAttachments", + "search": true, + "searchDescription": "Search for a VolumeAttachment using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/certificate.md b/docs.overmind.tech/docs/sources/stdlib/Types/certificate.md new file mode 100644 index 00000000..626f922a --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/certificate.md @@ -0,0 +1,13 @@ +--- +title: Certificate +sidebar_label: certificate +--- + +A Certificate resource represents an X.509 public-key certificate (typically served during a TLS/SSL handshake) together with any intermediate certificates that form its trust chain. Overmind analyses these certificates to surface risks such as imminent expiry, weak signature algorithms, incorrect key usage flags, or hostnames that do not match the Subject Alternative Names (SANs). +For the formal specification of X.509 certificates, see RFC 5280 – Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile: https://datatracker.ietf.org/doc/html/rfc5280 + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Takes a full certificate, or certificate bundle as input in PEM encoded format diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/dns.md b/docs.overmind.tech/docs/sources/stdlib/Types/dns.md new file mode 100644 index 00000000..e4193d4c --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/dns.md @@ -0,0 +1,27 @@ +--- +title: DNS Entry +sidebar_label: dns +--- + +The Domain Name System (DNS) translates human-readable names into machine-usable information. A DNS _A_ record maps a hostname to an IPv4 address, while an _AAAA_ record maps it to an IPv6 address. By querying these records, Overmind can reveal the infrastructure that a name ultimately points to, allowing you to spot configuration mistakes, dangling records, or unexpected dependencies before you deploy. +Reference documentation: RFC 1034 & RFC 1035 – Domain Names – Concepts and Facilities / Implementation and Specification (https://www.rfc-editor.org/rfc/rfc1034 and https://www.rfc-editor.org/rfc/rfc1035) + +## Supported Methods + +- `GET`: A DNS A or AAAA entry to look up +- ~~`LIST`~~ +- `SEARCH`: A DNS name (or IP for reverse DNS), this will perform a recursive search and return all results. It is recommended that you always use the SEARCH method + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +If the queried record is a CNAME, MX, NS or contains additional glue, Overmind follows those pointers and links the resulting records back as further `dns` items for deeper traversal. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +An A or AAAA record resolves to one or more IP addresses; each discovered address is linked as an `ip` item so their ownership, location and associated services can be examined. + +### [`rdap-domain`](/sources/stdlib/Types/rdap-domain) + +The second-level or higher-level domain extracted from the DNS name is linked to its corresponding `rdap-domain` item, giving visibility into registrar, registrant and name-server information that may present additional risk factors. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/http.md b/docs.overmind.tech/docs/sources/stdlib/Types/http.md new file mode 100644 index 00000000..5ae0276c --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/http.md @@ -0,0 +1,31 @@ +--- +title: HTTP Endpoint +sidebar_label: http +--- + +An HTTP Endpoint represents a reachable URL that Overmind can interrogate in order to discover configuration or security issues before deployment. By performing lightweight `HEAD` or `GET` requests, Overmind determines the availability, response headers, redirects, and TLS configuration (if the endpoint is served over HTTPS). This allows you to spot problems such as broken links, unexpected redirections, missing security headers, or invalid certificates early in the pipeline. +For more background on how HTTP endpoints are conventionally exposed and managed on the internet, refer to the W3C documentation on HTTP semantics: https://www.w3.org/Protocols/ (external). + +## Supported Methods + +- `GET`: A HTTP endpoint to run a `HEAD` request against +- ~~`LIST`~~ +- `SEARCH`: A HTTP URL to search for. Query parameters and fragments will be stripped from the URL before processing. + +## Possible Links + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The hostname or FQDN of the HTTP endpoint ultimately resolves to one or more IP addresses. Overmind records these addresses to understand network-level reachability and to cross-reference them with firewall, VPC or load-balancer configurations. + +### [`dns`](/sources/stdlib/Types/dns) + +Before an HTTP request can be made, the client performs a DNS lookup. Overmind connects the endpoint to its corresponding DNS records (A, AAAA, CNAME, etc.) so you can see how changes in DNS zone files might affect the endpoint’s availability. + +### [`certificate`](/sources/gcp/Types/gcp-compute-ssl-certificate) + +If the endpoint is accessed over HTTPS, the server presents an X.509 certificate. Overmind links the endpoint to the certificate resource it observes during the TLS handshake, enabling validation of expiry dates, issuer trust chains, and key strengths. + +### [`http`](/sources/stdlib/Types/http) + +HTTP endpoints often redirect to, embed, or call other HTTP endpoints (for example via 3xx redirects or links in HTML/JSON responses). Overmind establishes links between them so you can trace dependencies, spot redirect loops, and ensure downstream endpoints meet your security standards. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/ip.md b/docs.overmind.tech/docs/sources/stdlib/Types/ip.md new file mode 100644 index 00000000..693acc9a --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/ip.md @@ -0,0 +1,23 @@ +--- +title: IP Address +sidebar_label: ip +--- + +An IP address is a numerical label assigned to every device connected to an Internet Protocol network. It uniquely identifies the source and destination of traffic and is used for routing packets across interconnected networks. Overmind treats each IPv4 or IPv6 address that appears in your configuration as a discrete resource, allowing you to map how code, infrastructure and third-party services depend on it and to identify security or availability risks before deployment. +Official specification documents can be found in the relevant IETF RFCs: IPv4 is defined in RFC 791 and IPv6 in RFC 8200 (see https://www.rfc-editor.org/rfc/rfc791 and https://www.rfc-editor.org/rfc/rfc8200). + +## Supported Methods + +- `GET`: An ipv4 or ipv6 address +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +DNS records (such as A, AAAA or PTR) map human-readable hostnames to this IP address or resolve the address back to a hostname. Overmind links the `ip` resource to `dns` items whenever the address appears in one of these records so that you can trace how name resolution affects your deployment. + +### [`rdap-ip-network`](/sources/stdlib/Types/rdap-ip-network) + +Querying the Registration Data Access Protocol (RDAP) for an IP returns information about the allocation block, the organisation that owns it, contact details and abuse mailboxes. Overmind links an `ip` to the corresponding `rdap-ip-network` resource to surface ownership, geolocation and abuse-handling context that may influence compliance or threat-modelling decisions. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-asn.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-asn.md new file mode 100644 index 00000000..2ad5fc1a --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-asn.md @@ -0,0 +1,18 @@ +--- +title: Autonomous System Number (ASN) +sidebar_label: rdap-asn +--- + +An Autonomous System Number (ASN) is a unique 16- or 32-bit identifier assigned to an Autonomous System so that it can participate in Border Gateway Protocol (BGP) routing on the public Internet. Using the Registration Data Access Protocol (RDAP), you can query an ASN to obtain registration details such as the holder, allocation status, and associated contacts. For the formal specification of RDAP responses for ASNs, see [RFC 9083: Registration Data Access Protocol (RDAP)](https://datatracker.ietf.org/doc/html/rfc9083). + +## Supported Methods + +- `GET`: Get an ASN by handle i.e. "AS15169" +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`rdap-entity`](/sources/stdlib/Types/rdap-entity) + +An ASN RDAP record frequently contains an `entities` array. Each item in that array is an RDAP Entity object representing the organisation or individual responsible for the ASN (registrant, administrative contact, technical contact, etc.). Overmind therefore links an `rdap-asn` resource to one or more `rdap-entity` resources so that you can inspect the people or organisations behind a particular network. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-domain.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-domain.md new file mode 100644 index 00000000..f9f21abd --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-domain.md @@ -0,0 +1,31 @@ +--- +title: RDAP Domain +sidebar_label: rdap-domain +--- + +An RDAP Domain record represents the authoritative registration data for a domain name as returned by the Registration Data Access Protocol (RDAP). The record contains information such as the registrar, registrant and administrative contacts, name-servers, status flags (e.g. `clientTransferProhibited`), and important lifecycle dates (creation, expiry, last update). In Overmind the resource lets you inspect this registration data and understand how a domain fits into the rest of your deployment before any changes are made. +Official RDAP specification: https://www.rfc-editor.org/rfc/rfc9082 + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Search for a domain record by the domain name e.g. "www.google.com" + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +The name portion of the RDAP domain (e.g. `example.com`) will typically have authoritative DNS records such as `A`, `AAAA`, `MX`, etc. Overmind links the RDAP Domain to those `dns` items so that you can trace from the registration layer straight through to the operational zone file that will actually be served. + +### [`rdap-nameserver`](/sources/stdlib/Types/rdap-nameserver) + +An RDAP Domain record contains a list of host objects (name-servers) delegated for the zone. Each of those host objects is represented as an `rdap-nameserver` item. The link allows you to drill into the registration data for each individual name-server. + +### [`rdap-entity`](/sources/stdlib/Types/rdap-entity) + +Entities in RDAP describe people or organisations such as the registrant, administrative contact, or registrar. Overmind links the RDAP Domain to every referenced `rdap-entity` so that you can view contact details, roles and other domains controlled by the same party. + +### [`rdap-ip-network`](/sources/stdlib/Types/rdap-ip-network) + +If the RDAP Domain record (or any of its linked name-servers) includes embedded references to address space—commonly via `v4network` or `v6network` objects—Overmind exposes those as `rdap-ip-network` items. This lets you see which blocks of IP addresses are directly associated with the domain and whether they overlap with other infrastructure you manage. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-entity.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-entity.md new file mode 100644 index 00000000..1989ba3e --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-entity.md @@ -0,0 +1,20 @@ +--- +title: RDAP Entity +sidebar_label: rdap-entity +--- + +An RDAP (Registration Data Access Protocol) Entity resource represents a single contact object – either a person, organisation or role – that appears in the registration data held by Regional Internet Registries (RIRs) and other RDAP servers. It typically contains identifying information such as names, postal addresses, e-mail addresses, telephone numbers and public identifiers, and is referenced by other RDAP objects (e.g. ASNs, IPv4/IPv6 prefix ranges and domain names) as their administrative, technical or abuse contact. + +The formal structure and semantics of an RDAP Entity are defined in RFC 9083, section 5.1 (https://www.rfc-editor.org/rfc/rfc9083#section-5.1). + +## Supported Methods + +- `GET`: Get an entity by its handle. This method is discouraged as it's not reliable since entity bootstrapping isn't comprehensive +- ~~`LIST`~~ +- `SEARCH`: Search for an entity by its URL e.g. https://rdap.apnic.net/entity/AIC3-AP + +## Possible Links + +### [`rdap-asn`](/sources/stdlib/Types/rdap-asn) + +An ASN record can reference one or more RDAP Entities as its registrant, administrative or technical contacts. Overmind links the rdap-asn resource to the corresponding rdap-entity resources so that you can see who is responsible for a particular Autonomous System and assess any associated risk or exposure stemming from those contacts. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-ip-network.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-ip-network.md new file mode 100644 index 00000000..efe67c03 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-ip-network.md @@ -0,0 +1,19 @@ +--- +title: RDAP IP Network +sidebar_label: rdap-ip-network +--- + +An **RDAP IP Network** represents a block of IPv4 or IPv6 address space as returned by the Registration Data Access Protocol (RDAP). Overmind queries the authoritative RDAP service for a supplied address or prefix and surfaces the resulting network object, revealing who owns the range, the exact start- and end-addresses, its allocation status (allocated, assigned, reserved, etc.), and any policy or abuse information attached to it. Seeing this data in advance helps you verify that the addresses your deployment will use are valid and not bogon, reserved, or owned by an unexpected party. +The RDAP specification for IP networks is defined in [RFC 9083 – Registration Data Access Protocol (RDAP): Query Format](https://datatracker.ietf.org/doc/html/rfc9083). + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Search for the most specific network that contains the specified IP or CIDR + +## Possible Links + +### [`rdap-entity`](/sources/stdlib/Types/rdap-entity) + +An RDAP network record contains an `entities` array referencing the people, organisations, and roles (registrant, technical, abuse, etc.) responsible for the address space. Overmind links each of these references to its corresponding `rdap-entity` item, letting you inspect contact details and responsibility assignments related to the network. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-nameserver.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-nameserver.md new file mode 100644 index 00000000..663aa01a --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-nameserver.md @@ -0,0 +1,28 @@ +--- +title: RDAP Nameserver +sidebar_label: rdap-nameserver +--- + +The Registration Data Access Protocol (RDAP) is the modern, machine-readable replacement for the old WHOIS service. +An **RDAP ­nameserver resource** represents the authoritative information that a Top-Level Domain (TLD) registry publishes about a particular DNS nameserver. By querying this endpoint you can discover, for example, the registrar that manages the server, its associated IP addresses, its status with the registry and any abuse or support contacts. +For details of the protocol and the structure of a nameserver response, see the IETF specification: https://datatracker.ietf.org/doc/html/rfc7483#section-5.5. + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Search for the RDAP entry for a nameserver by its full URL e.g. "https://rdap.verisign.com/com/v1/nameserver/NS4.GOOGLE.COM" + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +A nameserver appears in DNS NS records. Overmind links the RDAP nameserver object to the corresponding `dns` item so that you can see which zones delegate to this server and whether those zones are also in your inventory. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The RDAP response normally includes the A and/or AAAA records for the nameserver. These addresses are represented as `ip` items, allowing you to trace from the logical nameserver to the concrete IP resources that sit behind it. + +### [`rdap-entity`](/sources/stdlib/Types/rdap-entity) + +Each nameserver RDAP document references one or more entities (registrar, registrant, technical contact, abuse contact, etc.). These are captured as separate `rdap-entity` items and linked so you can quickly identify who is responsible for the server and how to contact them. diff --git a/docs.overmind.tech/docs/sources/stdlib/_category_json b/docs.overmind.tech/docs/sources/stdlib/_category_json new file mode 100644 index 00000000..35c138aa --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/_category_json @@ -0,0 +1,9 @@ +{ + "label": "Public Resources (stdlib)", + "position": 3, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to explore with Overminds built-in Public Resoures." + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/certificate.json b/docs.overmind.tech/docs/sources/stdlib/data/certificate.json new file mode 100644 index 00000000..093db560 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/certificate.json @@ -0,0 +1,9 @@ +{ + "type": "certificate", + "category": 3, + "descriptiveName": "Certificate", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Takes a full certificate, or certificate bundle as input in PEM encoded format" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/dns.json b/docs.overmind.tech/docs/sources/stdlib/data/dns.json new file mode 100644 index 00000000..4bac0bc7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/dns.json @@ -0,0 +1,12 @@ +{ + "type": "dns", + "category": 3, + "potentialLinks": ["dns", "ip", "rdap-domain"], + "descriptiveName": "DNS Entry", + "supportedQueryMethods": { + "get": true, + "getDescription": "A DNS A or AAAA entry to look up", + "search": true, + "searchDescription": "A DNS name (or IP for reverse DNS), this will perform a recursive search and return all results. It is recommended that you always use the SEARCH method" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/http.json b/docs.overmind.tech/docs/sources/stdlib/data/http.json new file mode 100644 index 00000000..a7c70fb1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/http.json @@ -0,0 +1,12 @@ +{ + "type": "http", + "category": 3, + "potentialLinks": ["ip", "dns", "certificate", "http"], + "descriptiveName": "HTTP Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "A HTTP endpoint to run a `HEAD` request against", + "search": true, + "searchDescription": "A HTTP URL to search for. Query parameters and fragments will be stripped from the URL before processing." + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/ip.json b/docs.overmind.tech/docs/sources/stdlib/data/ip.json new file mode 100644 index 00000000..e0840b7b --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/ip.json @@ -0,0 +1,10 @@ +{ + "type": "ip", + "category": 3, + "potentialLinks": ["dns", "rdap-ip-network"], + "descriptiveName": "IP Address", + "supportedQueryMethods": { + "get": true, + "getDescription": "An ipv4 or ipv6 address" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-asn.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-asn.json new file mode 100644 index 00000000..41fe8522 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-asn.json @@ -0,0 +1,10 @@ +{ + "type": "rdap-asn", + "category": 3, + "potentialLinks": ["rdap-entity"], + "descriptiveName": "Autonomous System Number (ASN)", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ASN by handle i.e. \"AS15169\"" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-domain.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-domain.json new file mode 100644 index 00000000..6db55b25 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-domain.json @@ -0,0 +1,15 @@ +{ + "type": "rdap-domain", + "category": 3, + "potentialLinks": [ + "dns", + "rdap-nameserver", + "rdap-entity", + "rdap-ip-network" + ], + "descriptiveName": "RDAP Domain", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Search for a domain record by the domain name e.g. \"www.google.com\"" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-entity.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-entity.json new file mode 100644 index 00000000..705be5aa --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-entity.json @@ -0,0 +1,12 @@ +{ + "type": "rdap-entity", + "category": 4, + "potentialLinks": ["rdap-asn"], + "descriptiveName": "RDAP Entity", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an entity by its handle. This method is discouraged as it's not reliable since entity bootstrapping isn't comprehensive", + "search": true, + "searchDescription": "Search for an entity by its URL e.g. https://rdap.apnic.net/entity/AIC3-AP" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-ip-network.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-ip-network.json new file mode 100644 index 00000000..e08b0f84 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-ip-network.json @@ -0,0 +1,10 @@ +{ + "type": "rdap-ip-network", + "category": 3, + "potentialLinks": ["rdap-entity"], + "descriptiveName": "RDAP IP Network", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Search for the most specific network that contains the specified IP or CIDR" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-nameserver.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-nameserver.json new file mode 100644 index 00000000..9e1d927d --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-nameserver.json @@ -0,0 +1,10 @@ +{ + "type": "rdap-nameserver", + "category": 3, + "potentialLinks": ["dns", "ip", "rdap-entity"], + "descriptiveName": "RDAP Nameserver", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Search for the RDAP entry for a nameserver by its full URL e.g. \"https://rdap.verisign.com/com/v1/nameserver/NS4.GOOGLE.COM\"" + } +} diff --git a/go.mod b/go.mod index c076d262..94be0a7f 100644 --- a/go.mod +++ b/go.mod @@ -10,12 +10,13 @@ replace github.com/google/cel-go => github.com/google/cel-go v0.22.1 require ( atomicgo.dev/keyboard v0.2.9 - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 - buf.build/go/protovalidate v1.1.0 - cloud.google.com/go/aiplatform v1.115.0 + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 + buf.build/go/protovalidate v1.1.2 + cloud.google.com/go/aiplatform v1.116.0 cloud.google.com/go/auth v0.18.1 cloud.google.com/go/bigquery v1.73.1 cloud.google.com/go/bigtable v1.42.0 + cloud.google.com/go/certificatemanager v1.9.6 cloud.google.com/go/compute v1.54.0 cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/container v1.46.0 @@ -34,7 +35,8 @@ require ( cloud.google.com/go/run v1.15.0 cloud.google.com/go/secretmanager v1.16.0 cloud.google.com/go/securitycentermanagement v1.1.6 - cloud.google.com/go/spanner v1.87.0 + cloud.google.com/go/spanner v1.88.0 + cloud.google.com/go/storage v1.60.0 cloud.google.com/go/storagetransfer v1.13.1 connectrpc.com/connect v1.18.1 // v1.19.0 was faulty, wait until it is above this version github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 @@ -64,10 +66,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.53.1 github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.11 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.288.0 github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 github.com/aws/aws-sdk-go-v2/service/efs v1.41.10 - github.com/aws/aws-sdk-go-v2/service/eks v1.77.1 + github.com/aws/aws-sdk-go-v2/service/eks v1.80.0 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.19 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6 github.com/aws/aws-sdk-go-v2/service/iam v1.53.2 @@ -75,7 +77,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0 github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3 github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.4 - github.com/aws/aws-sdk-go-v2/service/rds v1.114.0 + github.com/aws/aws-sdk-go-v2/service/rds v1.115.0 github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 @@ -96,7 +98,10 @@ require ( github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/hcl/v2 v2.24.0 - github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77 + github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 + github.com/hashicorp/terraform-plugin-framework v1.17.0 + github.com/hashicorp/terraform-plugin-go v0.29.0 + github.com/hashicorp/terraform-plugin-testing v1.14.0 github.com/jedib0t/go-pretty/v6 v6.7.8 github.com/micahhausler/aws-iam-policy v0.4.2 github.com/miekg/dns v1.1.72 @@ -133,22 +138,22 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 - golang.org/x/net v0.49.0 - golang.org/x/oauth2 v0.34.0 + golang.org/x/net v0.50.0 + golang.org/x/oauth2 v0.35.0 golang.org/x/sync v0.19.0 - golang.org/x/text v0.33.0 + golang.org/x/text v0.34.0 gonum.org/v1/gonum v0.17.0 - google.golang.org/api v0.265.0 + google.golang.org/api v0.266.0 google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 - google.golang.org/grpc v1.78.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.1 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.35.0 - k8s.io/apimachinery v0.35.0 - k8s.io/client-go v0.35.0 - k8s.io/utils v0.0.0-20260108192941-914a6e750570 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 sigs.k8s.io/kind v0.31.0 ) @@ -156,13 +161,17 @@ require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/schedule v0.1.0 // indirect - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 cloud.google.com/go/longrunning v0.8.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/alecthomas/chroma/v2 v2.16.0 // indirect github.com/alecthomas/kingpin/v2 v2.4.0 // indirect @@ -193,10 +202,15 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/console v1.0.4 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -208,6 +222,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/cel-go v0.26.1 // indirect github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/gnostic-models v0.7.0 // indirect @@ -218,8 +233,25 @@ require ( github.com/gookit/color v1.5.4 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-cty v1.5.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hc-install v0.9.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/logutils v1.0.0 // indirect + github.com/hashicorp/terraform-exec v0.24.0 // indirect + github.com/hashicorp/terraform-json v0.27.2 // indirect + github.com/hashicorp/terraform-plugin-log v0.10.0 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 // indirect + github.com/hashicorp/terraform-registry-address v0.4.0 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -229,30 +261,40 @@ require ( github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sergi/go-diff v1.3.1 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed // indirect @@ -261,23 +303,26 @@ require ( github.com/yuin/goldmark-emoji v1.0.5 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/otel/log v0.11.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/schema v0.0.12 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.47.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/mod v0.32.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect - golang.org/x/term v0.39.0 // indirect + golang.org/x/term v0.40.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -285,6 +330,6 @@ require ( k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 15ea0ab2..15504935 100644 --- a/go.sum +++ b/go.sum @@ -8,16 +8,16 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= -buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY= -buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss= -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0= +buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/aiplatform v1.115.0 h1:m/dIJ/HixZDvHoXBGkA5Sd0RbiQp5lBVyddvR9uxHqI= -cloud.google.com/go/aiplatform v1.115.0/go.mod h1:DwPJAxebOTy6BajSMjF7ah3QvlYO4jf2gpJw6/1z9gU= +cloud.google.com/go/aiplatform v1.116.0 h1:Qc8tv4DD6IbQfDKDd1Hu2qeGeYxTKTeZ7GH0vQrLAm8= +cloud.google.com/go/aiplatform v1.116.0/go.mod h1:AdvoUUSXh9ykwEazibd3Fj6OUGrIiZwvZrvm4j5OdkU= cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -26,6 +26,8 @@ cloud.google.com/go/bigquery v1.73.1 h1:v//GZwdhtmCbZ87rOnxz7pectOGFS1GNRvrGTvLz cloud.google.com/go/bigquery v1.73.1/go.mod h1:KSLx1mKP/yGiA8U+ohSrqZM1WknUnjZAxHAQZ51/b1k= cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= cloud.google.com/go/bigtable v1.42.0/go.mod h1:oZ30nofVB6/UYGg7lBwGLWSea7NZUvw/WvBBgLY07xU= +cloud.google.com/go/certificatemanager v1.9.6 h1:v5X8X+THKrS9OFZb6k0GRDP1WQxLXTdMko7OInBliw4= +cloud.google.com/go/certificatemanager v1.9.6/go.mod h1:vWogV874jKZkSRDFCMM3r7wqybv8WXs3XhyNff6o/Zo= cloud.google.com/go/compute v1.54.0 h1:4CKmnpO+40z44bKG5bdcKxQ7ocNpRtOc9SCLLUzze1w= cloud.google.com/go/compute v1.54.0/go.mod h1:RfBj0L1x/pIM84BrzNX2V21oEv16EKRPBiTcBRRH1Ww= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= @@ -66,14 +68,18 @@ cloud.google.com/go/secretmanager v1.16.0 h1:19QT7ZsLJ8FSP1k+4esQvuCD7npMJml6hYz cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q= cloud.google.com/go/securitycentermanagement v1.1.6 h1:XFqjKq4ZpKTj8xCXWs/mTmh/UMWDiV25iCOUd9xaGWI= cloud.google.com/go/securitycentermanagement v1.1.6/go.mod h1:nt5Z6rU4s2/j8R/EQxG5K7OfVAfAfwo89j0Nx2Srzaw= -cloud.google.com/go/spanner v1.87.0 h1:M9RGcj/4gJk6yY1lRLOz1Ze+5ufoWhbIiurzXLOOfcw= -cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= -cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8= -cloud.google.com/go/storage v1.59.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU= +cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= +cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8= +cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= cloud.google.com/go/storagetransfer v1.13.1 h1:Sjukr1LtUt7vLTHNvGc2gaAqlXNFeDFRIRmWGrFaJlY= cloud.google.com/go/storagetransfer v1.13.1/go.mod h1:S858w5l383ffkdqAqrAA+BC7KlhCqeNieK3sFf5Bj4Y= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= @@ -120,10 +126,12 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -135,8 +143,12 @@ github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/MrAlias/otel-schema-utils v0.4.0-alpha h1:6ZG9rw4NvxKwRp2Bmnfr8WJZVWLhK4e5n3+ezXE6Z2g= github.com/MrAlias/otel-schema-utils v0.4.0-alpha/go.mod h1:baehOhES9qiLv9xMcsY6ZQlKLBRR89XVJEvU7Yz3qJk= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= @@ -155,6 +167,7 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= @@ -190,14 +203,14 @@ github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.11 h1:3+DkKJAq5VVqPNu3e github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.11/go.mod h1:DNG3VkdVy874VMHH46ekGsD3nq6D4tyDV3HIOuVoouM= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0 h1:CyYoeHWjVSGimzMhlL0Z4l5gLCa++ccnRJKrsaNssxE= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0/go.mod h1:ctEsEHY2vFQc6i4KU07q4n68v7BAmTbujv2Y+z8+hQY= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0 h1:cRZQsqCy59DSJmvmUYzi9K+dutysXzfx6F+fkcIHtOk= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.288.0 h1:cRu1CgKDK0qYNJRZBWaktwGZ6fvcFiKZm1Huzesc47s= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.288.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY= github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 h1:MzP/ElwTpINq+hS80ZQz4epKVnUTlz8Sz+P/AFORCKM= github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0/go.mod h1:pMlGFDpHoLTJOIZHGdJOAWmi+xeIlQXuFTuQxs1epYE= github.com/aws/aws-sdk-go-v2/service/efs v1.41.10 h1:7ixaaFyZ8xXJWPcK3qQKFf1k1HgME9rtCY7S6Unih8I= github.com/aws/aws-sdk-go-v2/service/efs v1.41.10/go.mod h1:QwCUd/L5/HX4s/uWt3LPEOwQb/AYE4OyMGB8SL9/W4Y= -github.com/aws/aws-sdk-go-v2/service/eks v1.77.1 h1:pMXNbXUX4Xd9fRmRdEe/vQ/5EFRy2M4jvW6geO5lhd8= -github.com/aws/aws-sdk-go-v2/service/eks v1.77.1/go.mod h1:Qg678m+87sCuJhcsZojenz8mblYG+Tq86V4m3hjVz0s= +github.com/aws/aws-sdk-go-v2/service/eks v1.80.0 h1:moQGV8cPbVTN7r2Xte1Mybku35QDePSJEd3onYVmBtY= +github.com/aws/aws-sdk-go-v2/service/eks v1.80.0/go.mod h1:Qg678m+87sCuJhcsZojenz8mblYG+Tq86V4m3hjVz0s= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.19 h1:ybEda2mkkX2o8NadXZBtcO9tgmW9cTQgeVSjypNsAy0= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.19/go.mod h1:RiMytGvN4azx4yLM0Kn3bX/XO9dLxj+eG72Smy+vNzI= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6 h1:fQR1aeZKaiPkNPya0JMy2nhsoqoSgIWc3/QTiTiL1K0= @@ -222,8 +235,8 @@ github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3 h1:Fobn9IdJv8lgpGv5 github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3/go.mod h1:1Yhak+i7rIt8Yq2lWViNXI4zoMufmqqjR89vNwgzafw= github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.4 h1:J38JaWrNRBxSU/nrrC92/jqGVl07RAdGXM9GvwtdQqE= github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.4/go.mod h1:vdT+5yxPXmxzJ8ETFpajcjce/eUViRAG58SPtZyHoGA= -github.com/aws/aws-sdk-go-v2/service/rds v1.114.0 h1:p9c6HDzx6sTf7uyc9xsQd693uzArsPrsVr9n0oRk7DU= -github.com/aws/aws-sdk-go-v2/service/rds v1.114.0/go.mod h1:JBRYWpz5oXQtHgQC+X8LX9lh0FBCwRHJlWEIT+TTLaE= +github.com/aws/aws-sdk-go-v2/service/rds v1.115.0 h1:oNl6YghOtxu3MiFk1tQ86QlrYMIEJazGUDbBCg9nxLA= +github.com/aws/aws-sdk-go-v2/service/rds v1.115.0/go.mod h1:JBRYWpz5oXQtHgQC+X8LX9lh0FBCwRHJlWEIT+TTLaE= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= @@ -252,6 +265,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -274,14 +289,18 @@ github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1 h1:8fUBS github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -290,13 +309,19 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= -github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -311,6 +336,12 @@ github.com/getsentry/sentry-go v0.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7 github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= +github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -334,6 +365,11 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -344,6 +380,8 @@ github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8i github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= @@ -373,30 +411,77 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0= +github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= +github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= -github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77 h1:JyCyXTn0iSHO66Gy5D+4Q031oqRBSRrARILrc1NFu2U= -github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 h1:p+oHuSCXvfFBFAejlPswDa7i5fi3r3+03jeW9mJs4qM= +github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= +github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= +github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= +github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= +github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/hashicorp/terraform-plugin-framework v1.17.0 h1:JdX50CFrYcYFY31gkmitAEAzLKoBgsK+iaJjDC8OexY= +github.com/hashicorp/terraform-plugin-framework v1.17.0/go.mod h1:4OUXKdHNosX+ys6rLgVlgklfxN3WHR5VHSOABeS/BM0= +github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU= +github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM= +github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= +github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU= +github.com/hashicorp/terraform-plugin-testing v1.14.0 h1:5t4VKrjOJ0rg0sVuSJ86dz5K7PHsMO6OKrHFzDBerWA= +github.com/hashicorp/terraform-plugin-testing v1.14.0/go.mod h1:1qfWkecyYe1Do2EEOK/5/WnTyvC8wQucUkkhiGLg5nk= +github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= +github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= +github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= +github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= @@ -421,8 +506,13 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -437,10 +527,18 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -465,6 +563,8 @@ github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= @@ -481,6 +581,8 @@ github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -513,10 +615,12 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -544,6 +648,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -558,8 +663,17 @@ github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z7 github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xiam/dig v0.0.0-20191116195832-893b5fb5093b h1:ajy6PPLDeQaf7xf4P/4Ie/wsUTEqjy3Irl+xFelmjk0= @@ -589,8 +703,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0-20250901115419-474a7992e57c h1:YSqSR1Fil5Ip0N6AlNBFbNv7cvIHZ2j4+wqbVafAGmQ= go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0-20250901115419-474a7992e57c/go.mod h1:avnUkmc6cwMhcExsYaSv0SQVqygTfXGTn41eZ7xjKpo= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= @@ -601,6 +715,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQg go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= @@ -629,42 +745,50 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -672,15 +796,16 @@ golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -690,23 +815,30 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU= -google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY= +google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk= +google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -718,32 +850,33 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= -k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= -k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= -k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= -k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= -k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g= sigs.k8s.io/kind v0.31.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/auth/auth.go b/go/auth/auth.go similarity index 99% rename from auth/auth.go rename to go/auth/auth.go index 6e1bd323..9b7e2ce2 100644 --- a/auth/auth.go +++ b/go/auth/auth.go @@ -14,9 +14,9 @@ import ( josejwt "github.com/go-jose/go-jose/v4/jwt" "github.com/nats-io/jwt/v2" "github.com/nats-io/nkeys" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/codes" diff --git a/auth/auth_client.go b/go/auth/auth_client.go similarity index 98% rename from auth/auth_client.go rename to go/auth/auth_client.go index 73b5dc08..f5be155f 100644 --- a/auth/auth_client.go +++ b/go/auth/auth_client.go @@ -5,8 +5,8 @@ import ( "fmt" "net/http" - "github.com/overmindtech/cli/sdp-go/sdpconnect" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" ) diff --git a/auth/auth_test.go b/go/auth/auth_test.go similarity index 97% rename from auth/auth_test.go rename to go/auth/auth_test.go index 62c1c835..f8d85bae 100644 --- a/auth/auth_test.go +++ b/go/auth/auth_test.go @@ -12,8 +12,8 @@ import ( "connectrpc.com/connect" "github.com/nats-io/nkeys" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" ) var tokenExchangeURLs = []string{ @@ -65,7 +65,7 @@ func GetTestOAuthTokenClient(t *testing.T) *natsTokenClient { var clientSecret string var exists bool - errorFormat := "environment variable %v not found. Set up your test environment first. See: https://github.com/overmindtech/cli/auth0-test-data" + errorFormat := "environment variable %v not found. Set up your test environment first. See: https://github.com/overmindtech/cli/go/auth0-test-data" // Read secrets form the environment if domain, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_DOMAIN"); !exists || domain == "" { diff --git a/auth/gcpauth.go b/go/auth/gcpauth.go similarity index 100% rename from auth/gcpauth.go rename to go/auth/gcpauth.go diff --git a/auth/middleware.go b/go/auth/middleware.go similarity index 100% rename from auth/middleware.go rename to go/auth/middleware.go diff --git a/auth/middleware_test.go b/go/auth/middleware_test.go similarity index 100% rename from auth/middleware_test.go rename to go/auth/middleware_test.go diff --git a/auth/nats.go b/go/auth/nats.go similarity index 96% rename from auth/nats.go rename to go/auth/nats.go index 2e3ef9ee..40c0aade 100644 --- a/auth/nats.go +++ b/go/auth/nats.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/nats-io/nats.go" @@ -220,10 +220,17 @@ func (o NATSOptions) Connect() (sdp.EncodedConnection, error) { var err error for triesLeft != 0 { - triesLeft-- + if triesLeft > 0 { + triesLeft-- + } + // Log a non-negative value: 0 means unlimited retries (NumRetries < 0) + logTriesLeft := triesLeft + if logTriesLeft < 0 { + logTriesLeft = 0 + } lf := log.Fields{ "servers": servers, - "triesLeft": triesLeft, + "triesLeft": logTriesLeft, } log.WithFields(lf).Info("NATS connecting") diff --git a/auth/nats_test.go b/go/auth/nats_test.go similarity index 99% rename from auth/nats_test.go rename to go/auth/nats_test.go index 7be1fae4..d286245a 100644 --- a/auth/nats_test.go +++ b/go/auth/nats_test.go @@ -11,7 +11,7 @@ import ( "github.com/nats-io/jwt/v2" "github.com/nats-io/nats.go" "github.com/nats-io/nkeys" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestToNatsOptions(t *testing.T) { diff --git a/auth/tracing.go b/go/auth/tracing.go similarity index 84% rename from auth/tracing.go rename to go/auth/tracing.go index d85f3bdd..6a778742 100644 --- a/auth/tracing.go +++ b/go/auth/tracing.go @@ -7,7 +7,7 @@ import ( ) const ( - instrumentationName = "github.com/overmindtech/cli/auth" + instrumentationName = "github.com/overmindtech/cli/go/auth" instrumentationVersion = "0.0.1" ) diff --git a/discovery/adapter.go b/go/discovery/adapter.go similarity index 98% rename from discovery/adapter.go rename to go/discovery/adapter.go index 5d0b473d..65821583 100644 --- a/discovery/adapter.go +++ b/go/discovery/adapter.go @@ -5,8 +5,8 @@ import ( "slices" "sync" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // Adapter is capable of finding information about items diff --git a/discovery/adapter_test.go b/go/discovery/adapter_test.go similarity index 99% rename from discovery/adapter_test.go rename to go/discovery/adapter_test.go index 34011567..fcf0ec91 100644 --- a/discovery/adapter_test.go +++ b/go/discovery/adapter_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestEngineAddAdapters(t *testing.T) { diff --git a/discovery/adapterhost.go b/go/discovery/adapterhost.go similarity index 99% rename from discovery/adapterhost.go rename to go/discovery/adapterhost.go index 9d34eee4..18014515 100644 --- a/discovery/adapterhost.go +++ b/go/discovery/adapterhost.go @@ -6,7 +6,7 @@ import ( "strings" "sync" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "google.golang.org/protobuf/proto" ) diff --git a/discovery/adapterhost_bench_test.go b/go/discovery/adapterhost_bench_test.go similarity index 99% rename from discovery/adapterhost_bench_test.go rename to go/discovery/adapterhost_bench_test.go index fc6b09da..91501d79 100644 --- a/discovery/adapterhost_bench_test.go +++ b/go/discovery/adapterhost_bench_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/sourcegraph/conc/pool" ) diff --git a/discovery/adapterhost_test.go b/go/discovery/adapterhost_test.go similarity index 99% rename from discovery/adapterhost_test.go rename to go/discovery/adapterhost_test.go index c706954f..74199b99 100644 --- a/discovery/adapterhost_test.go +++ b/go/discovery/adapterhost_test.go @@ -3,7 +3,7 @@ package discovery import ( "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestAdapterHostExpandQuery(t *testing.T) { diff --git a/discovery/cmd.go b/go/discovery/cmd.go similarity index 98% rename from discovery/cmd.go rename to go/discovery/cmd.go index 27b2f863..c2b4fe7f 100644 --- a/discovery/cmd.go +++ b/go/discovery/cmd.go @@ -12,9 +12,9 @@ import ( "github.com/getsentry/sentry-go" "github.com/google/uuid" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/discovery/cmd_test.go b/go/discovery/cmd_test.go similarity index 99% rename from discovery/cmd_test.go rename to go/discovery/cmd_test.go index 49a433f4..1bbc1b0a 100644 --- a/discovery/cmd_test.go +++ b/go/discovery/cmd_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/discovery/doc.go b/go/discovery/doc.go similarity index 100% rename from discovery/doc.go rename to go/discovery/doc.go diff --git a/discovery/engine.go b/go/discovery/engine.go similarity index 98% rename from discovery/engine.go rename to go/discovery/engine.go index b9ea9e4a..1b64388e 100644 --- a/discovery/engine.go +++ b/go/discovery/engine.go @@ -15,9 +15,9 @@ import ( "github.com/getsentry/sentry-go" "github.com/google/uuid" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" "go.opentelemetry.io/otel/attribute" @@ -869,20 +869,19 @@ func (e *Engine) ReadinessProbeHandlerFunc() func(http.ResponseWriter, *http.Req } // ServeHealthProbes starts an HTTP server for Kubernetes health probes on the given port. -// Registers /healthz/alive (liveness), /healthz/ready (readiness), and /healthz (backward compat). +// Registers /healthz/alive (liveness) and /healthz/ready (readiness). // Runs in a goroutine. Use for sources that only need health checks on the given port. func (e *Engine) ServeHealthProbes(port int) { mux := http.NewServeMux() mux.HandleFunc("/healthz/alive", e.LivenessProbeHandlerFunc()) mux.HandleFunc("/healthz/ready", e.ReadinessProbeHandlerFunc()) - mux.HandleFunc("/healthz", e.LivenessProbeHandlerFunc()) logFields := log.Fields{"port": port} if e.EngineConfig != nil { logFields["ovm.engine.type"] = e.EngineConfig.EngineType logFields["ovm.engine.name"] = e.EngineConfig.SourceName } - log.WithFields(logFields).Debug("Starting healthcheck server with endpoints: /healthz/alive, /healthz/ready, /healthz") + log.WithFields(logFields).Debug("Starting healthcheck server with endpoints: /healthz/alive, /healthz/ready") go func() { defer sentry.Recover() diff --git a/discovery/engine_initerror_test.go b/go/discovery/engine_initerror_test.go similarity index 99% rename from discovery/engine_initerror_test.go rename to go/discovery/engine_initerror_test.go index 37ed2404..26acfa34 100644 --- a/discovery/engine_initerror_test.go +++ b/go/discovery/engine_initerror_test.go @@ -10,7 +10,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestSetInitError(t *testing.T) { diff --git a/discovery/engine_test.go b/go/discovery/engine_test.go similarity index 99% rename from discovery/engine_test.go rename to go/discovery/engine_test.go index 8dd7ad6d..f90ae870 100644 --- a/discovery/engine_test.go +++ b/go/discovery/engine_test.go @@ -11,9 +11,9 @@ import ( "github.com/google/uuid" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats-server/v2/test" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "golang.org/x/oauth2" ) @@ -762,7 +762,7 @@ func GetTestOAuthTokenClient(t *testing.T, account string) auth.TokenClient { var clientSecret string var exists bool - errorFormat := "environment variable %v not found. Set up your test environment first. See: https://github.com/overmindtech/cli/auth0-test-data" + errorFormat := "environment variable %v not found. Set up your test environment first. See: https://github.com/overmindtech/cli/go/auth0-test-data" // Read secrets form the environment if domain, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_DOMAIN"); !exists || domain == "" { diff --git a/discovery/enginerequests.go b/go/discovery/enginerequests.go similarity index 98% rename from discovery/enginerequests.go rename to go/discovery/enginerequests.go index c3bf6856..3b04bf82 100644 --- a/discovery/enginerequests.go +++ b/go/discovery/enginerequests.go @@ -10,8 +10,8 @@ import ( "github.com/google/uuid" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" "go.opentelemetry.io/otel/attribute" @@ -113,7 +113,6 @@ func (e *Engine) HandleQuery(ctx context.Context, query *sdp.Query) { if query.GetRecursionBehaviour() != nil { span.SetAttributes( attribute.Int("ovm.sdp.linkDepth", int(query.GetRecursionBehaviour().GetLinkDepth())), - attribute.Bool("ovm.sdp.followOnlyBlastPropagation", query.GetRecursionBehaviour().GetFollowOnlyBlastPropagation()), ) } diff --git a/discovery/enginerequests_test.go b/go/discovery/enginerequests_test.go similarity index 98% rename from discovery/enginerequests_test.go rename to go/discovery/enginerequests_test.go index 8dfb2787..a8894d05 100644 --- a/discovery/enginerequests_test.go +++ b/go/discovery/enginerequests_test.go @@ -7,10 +7,10 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/tracing" "github.com/sourcegraph/conc/pool" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/discovery/getfindmutex.go b/go/discovery/getfindmutex.go similarity index 100% rename from discovery/getfindmutex.go rename to go/discovery/getfindmutex.go diff --git a/discovery/getfindmutex_test.go b/go/discovery/getfindmutex_test.go similarity index 100% rename from discovery/getfindmutex_test.go rename to go/discovery/getfindmutex_test.go diff --git a/discovery/heartbeat.go b/go/discovery/heartbeat.go similarity index 98% rename from discovery/heartbeat.go rename to go/discovery/heartbeat.go index ee7563ad..0ae6a862 100644 --- a/discovery/heartbeat.go +++ b/go/discovery/heartbeat.go @@ -7,8 +7,8 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/discovery/heartbeat_test.go b/go/discovery/heartbeat_test.go similarity index 99% rename from discovery/heartbeat_test.go rename to go/discovery/heartbeat_test.go index 74b79e0b..94e55915 100644 --- a/discovery/heartbeat_test.go +++ b/go/discovery/heartbeat_test.go @@ -8,7 +8,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type testHeartbeatClient struct { diff --git a/discovery/item_tests.go b/go/discovery/item_tests.go similarity index 98% rename from discovery/item_tests.go rename to go/discovery/item_tests.go index 6627154d..d959bf8e 100644 --- a/discovery/item_tests.go +++ b/go/discovery/item_tests.go @@ -5,7 +5,7 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) var RFC1123 = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`) diff --git a/discovery/logs.go b/go/discovery/logs.go similarity index 98% rename from discovery/logs.go rename to go/discovery/logs.go index ebb46d09..3e3ab276 100644 --- a/discovery/logs.go +++ b/go/discovery/logs.go @@ -5,7 +5,7 @@ import ( "errors" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // LogAdapter is a singleton from the source that handles GetLogRecordsRequest diff --git a/discovery/logs_test.go b/go/discovery/logs_test.go similarity index 99% rename from discovery/logs_test.go rename to go/discovery/logs_test.go index 33c0d00d..6dfc7efe 100644 --- a/discovery/logs_test.go +++ b/go/discovery/logs_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/discovery/main_test.go b/go/discovery/main_test.go similarity index 89% rename from discovery/main_test.go rename to go/discovery/main_test.go index bcaee571..9b2a75ca 100644 --- a/discovery/main_test.go +++ b/go/discovery/main_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) func TestMain(m *testing.M) { diff --git a/discovery/nats_shared_test.go b/go/discovery/nats_shared_test.go similarity index 100% rename from discovery/nats_shared_test.go rename to go/discovery/nats_shared_test.go diff --git a/discovery/nats_watcher.go b/go/discovery/nats_watcher.go similarity index 100% rename from discovery/nats_watcher.go rename to go/discovery/nats_watcher.go diff --git a/discovery/nats_watcher_test.go b/go/discovery/nats_watcher_test.go similarity index 100% rename from discovery/nats_watcher_test.go rename to go/discovery/nats_watcher_test.go diff --git a/discovery/nil_publisher.go b/go/discovery/nil_publisher.go similarity index 98% rename from discovery/nil_publisher.go rename to go/discovery/nil_publisher.go index 15119900..5e64d34b 100644 --- a/discovery/nil_publisher.go +++ b/go/discovery/nil_publisher.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "google.golang.org/protobuf/proto" ) diff --git a/discovery/performance_test.go b/go/discovery/performance_test.go similarity index 98% rename from discovery/performance_test.go rename to go/discovery/performance_test.go index a6b4bd8f..fd521c71 100644 --- a/discovery/performance_test.go +++ b/go/discovery/performance_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" ) type SlowAdapter struct { diff --git a/discovery/querytracker.go b/go/discovery/querytracker.go similarity index 97% rename from discovery/querytracker.go rename to go/discovery/querytracker.go index d7bb9a1f..4cb30c97 100644 --- a/discovery/querytracker.go +++ b/go/discovery/querytracker.go @@ -4,8 +4,8 @@ import ( "context" "errors" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/discovery/querytracker_test.go b/go/discovery/querytracker_test.go similarity index 98% rename from discovery/querytracker_test.go rename to go/discovery/querytracker_test.go index 9a8a31a5..338e29f8 100644 --- a/discovery/querytracker_test.go +++ b/go/discovery/querytracker_test.go @@ -7,8 +7,8 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/discovery/shared_test.go b/go/discovery/shared_test.go similarity index 98% rename from discovery/shared_test.go rename to go/discovery/shared_test.go index ce2943f5..b4e42d99 100644 --- a/discovery/shared_test.go +++ b/go/discovery/shared_test.go @@ -9,8 +9,8 @@ import ( "time" "github.com/goombaio/namegenerator" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/discovery/tracing.go b/go/discovery/tracing.go similarity index 82% rename from discovery/tracing.go rename to go/discovery/tracing.go index d2d90396..9a5783de 100644 --- a/discovery/tracing.go +++ b/go/discovery/tracing.go @@ -7,7 +7,7 @@ import ( ) const ( - instrumentationName = "github.com/overmindtech/cli/discovery/discovery" + instrumentationName = "github.com/overmindtech/cli/go/discovery/discovery" instrumentationVersion = "0.0.1" ) diff --git a/logging/logging.go b/go/logging/logging.go similarity index 100% rename from logging/logging.go rename to go/logging/logging.go diff --git a/logging/logging_test.go b/go/logging/logging_test.go similarity index 100% rename from logging/logging_test.go rename to go/logging/logging_test.go diff --git a/sdp-go/.gitignore b/go/sdp-go/.gitignore similarity index 100% rename from sdp-go/.gitignore rename to go/sdp-go/.gitignore diff --git a/sdp-go/account.go b/go/sdp-go/account.go similarity index 100% rename from sdp-go/account.go rename to go/sdp-go/account.go diff --git a/sdp-go/account.pb.go b/go/sdp-go/account.pb.go similarity index 95% rename from sdp-go/account.pb.go rename to go/sdp-go/account.pb.go index 679cfe1f..8f4cd406 100644 --- a/sdp-go/account.pb.go +++ b/go/sdp-go/account.pb.go @@ -3804,6 +3804,86 @@ func (*UnsetGithubInstallationIDResponse) Descriptor() ([]byte, []int) { return file_account_proto_rawDescGZIP(), []int{65} } +type GetOrCreateAWSExternalIdRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOrCreateAWSExternalIdRequest) Reset() { + *x = GetOrCreateAWSExternalIdRequest{} + mi := &file_account_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOrCreateAWSExternalIdRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOrCreateAWSExternalIdRequest) ProtoMessage() {} + +func (x *GetOrCreateAWSExternalIdRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOrCreateAWSExternalIdRequest.ProtoReflect.Descriptor instead. +func (*GetOrCreateAWSExternalIdRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{66} +} + +type GetOrCreateAWSExternalIdResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AwsExternalId string `protobuf:"bytes,1,opt,name=aws_external_id,json=awsExternalId,proto3" json:"aws_external_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOrCreateAWSExternalIdResponse) Reset() { + *x = GetOrCreateAWSExternalIdResponse{} + mi := &file_account_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOrCreateAWSExternalIdResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOrCreateAWSExternalIdResponse) ProtoMessage() {} + +func (x *GetOrCreateAWSExternalIdResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOrCreateAWSExternalIdResponse.ProtoReflect.Descriptor instead. +func (*GetOrCreateAWSExternalIdResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{67} +} + +func (x *GetOrCreateAWSExternalIdResponse) GetAwsExternalId() string { + if x != nil { + return x.AwsExternalId + } + return "" +} + // Team member related messages type ListTeamMembersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3813,7 +3893,7 @@ type ListTeamMembersRequest struct { func (x *ListTeamMembersRequest) Reset() { *x = ListTeamMembersRequest{} - mi := &file_account_proto_msgTypes[66] + mi := &file_account_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3825,7 +3905,7 @@ func (x *ListTeamMembersRequest) String() string { func (*ListTeamMembersRequest) ProtoMessage() {} func (x *ListTeamMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[66] + mi := &file_account_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3838,7 +3918,7 @@ func (x *ListTeamMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTeamMembersRequest.ProtoReflect.Descriptor instead. func (*ListTeamMembersRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{66} + return file_account_proto_rawDescGZIP(), []int{68} } type ListTeamMembersResponse struct { @@ -3850,7 +3930,7 @@ type ListTeamMembersResponse struct { func (x *ListTeamMembersResponse) Reset() { *x = ListTeamMembersResponse{} - mi := &file_account_proto_msgTypes[67] + mi := &file_account_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3862,7 +3942,7 @@ func (x *ListTeamMembersResponse) String() string { func (*ListTeamMembersResponse) ProtoMessage() {} func (x *ListTeamMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[67] + mi := &file_account_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3875,7 +3955,7 @@ func (x *ListTeamMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTeamMembersResponse.ProtoReflect.Descriptor instead. func (*ListTeamMembersResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{67} + return file_account_proto_rawDescGZIP(), []int{69} } func (x *ListTeamMembersResponse) GetMembers() []*TeamMember { @@ -3899,7 +3979,7 @@ type TeamMember struct { func (x *TeamMember) Reset() { *x = TeamMember{} - mi := &file_account_proto_msgTypes[68] + mi := &file_account_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3911,7 +3991,7 @@ func (x *TeamMember) String() string { func (*TeamMember) ProtoMessage() {} func (x *TeamMember) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[68] + mi := &file_account_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3924,7 +4004,7 @@ func (x *TeamMember) ProtoReflect() protoreflect.Message { // Deprecated: Use TeamMember.ProtoReflect.Descriptor instead. func (*TeamMember) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{68} + return file_account_proto_rawDescGZIP(), []int{70} } func (x *TeamMember) GetUUID() []byte { @@ -3956,7 +4036,7 @@ type GetWelcomeScreenInformationRequest struct { func (x *GetWelcomeScreenInformationRequest) Reset() { *x = GetWelcomeScreenInformationRequest{} - mi := &file_account_proto_msgTypes[69] + mi := &file_account_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3968,7 +4048,7 @@ func (x *GetWelcomeScreenInformationRequest) String() string { func (*GetWelcomeScreenInformationRequest) ProtoMessage() {} func (x *GetWelcomeScreenInformationRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[69] + mi := &file_account_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3981,7 +4061,7 @@ func (x *GetWelcomeScreenInformationRequest) ProtoReflect() protoreflect.Message // Deprecated: Use GetWelcomeScreenInformationRequest.ProtoReflect.Descriptor instead. func (*GetWelcomeScreenInformationRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{69} + return file_account_proto_rawDescGZIP(), []int{71} } type InviterInformation struct { @@ -3995,7 +4075,7 @@ type InviterInformation struct { func (x *InviterInformation) Reset() { *x = InviterInformation{} - mi := &file_account_proto_msgTypes[70] + mi := &file_account_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4007,7 +4087,7 @@ func (x *InviterInformation) String() string { func (*InviterInformation) ProtoMessage() {} func (x *InviterInformation) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[70] + mi := &file_account_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4020,7 +4100,7 @@ func (x *InviterInformation) ProtoReflect() protoreflect.Message { // Deprecated: Use InviterInformation.ProtoReflect.Descriptor instead. func (*InviterInformation) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{70} + return file_account_proto_rawDescGZIP(), []int{72} } func (x *InviterInformation) GetEmail() string { @@ -4053,7 +4133,7 @@ type GetWelcomeScreenInformationResponse struct { func (x *GetWelcomeScreenInformationResponse) Reset() { *x = GetWelcomeScreenInformationResponse{} - mi := &file_account_proto_msgTypes[71] + mi := &file_account_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4065,7 +4145,7 @@ func (x *GetWelcomeScreenInformationResponse) String() string { func (*GetWelcomeScreenInformationResponse) ProtoMessage() {} func (x *GetWelcomeScreenInformationResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[71] + mi := &file_account_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4078,7 +4158,7 @@ func (x *GetWelcomeScreenInformationResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use GetWelcomeScreenInformationResponse.ProtoReflect.Descriptor instead. func (*GetWelcomeScreenInformationResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{71} + return file_account_proto_rawDescGZIP(), []int{73} } func (x *GetWelcomeScreenInformationResponse) GetInviterInformation() *InviterInformation { @@ -4310,7 +4390,10 @@ const file_account_proto_rawDesc = "" + "\x16github_installation_id\x18\x01 \x01(\x03R\x14githubInstallationId\"!\n" + "\x1fSetGithubInstallationIDResponse\"\"\n" + " UnsetGithubInstallationIDRequest\"#\n" + - "!UnsetGithubInstallationIDResponse\"\x18\n" + + "!UnsetGithubInstallationIDResponse\"!\n" + + "\x1fGetOrCreateAWSExternalIdRequest\"J\n" + + " GetOrCreateAWSExternalIdResponse\x12&\n" + + "\x0faws_external_id\x18\x01 \x01(\tR\rawsExternalId\"\x18\n" + "\x16ListTeamMembersRequest\"H\n" + "\x17ListTeamMembersResponse\x12-\n" + "\amembers\x18\x01 \x03(\v2\x13.account.TeamMemberR\amembers\"U\n" + @@ -4369,7 +4452,7 @@ const file_account_proto_rawDesc = "" + "\fUpdateSource\x12!.account.AdminUpdateSourceRequest\x1a\x1d.account.UpdateSourceResponse\x12P\n" + "\fDeleteSource\x12!.account.AdminDeleteSourceRequest\x1a\x1d.account.DeleteSourceResponse\x12\\\n" + "\x10KeepaliveSources\x12%.account.AdminKeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponse\x12M\n" + - "\vCreateToken\x12 .account.AdminCreateTokenRequest\x1a\x1c.account.CreateTokenResponse2\x98\x0f\n" + + "\vCreateToken\x12 .account.AdminCreateTokenRequest\x1a\x1c.account.CreateTokenResponse2\x89\x10\n" + "\x11ManagementService\x12E\n" + "\n" + "GetAccount\x12\x1a.account.GetAccountRequest\x1a\x1b.account.GetAccountResponse\x12N\n" + @@ -4392,7 +4475,8 @@ const file_account_proto_rawDesc = "" + "\x0fListTeamMembers\x12\x1f.account.ListTeamMembersRequest\x1a .account.ListTeamMembersResponse\x12x\n" + "\x1bGetWelcomeScreenInformation\x12+.account.GetWelcomeScreenInformationRequest\x1a,.account.GetWelcomeScreenInformationResponse\x12l\n" + "\x17SetGithubInstallationID\x12'.account.SetGithubInstallationIDRequest\x1a(.account.SetGithubInstallationIDResponse\x12r\n" + - "\x19UnsetGithubInstallationID\x12).account.UnsetGithubInstallationIDRequest\x1a*.account.UnsetGithubInstallationIDResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x19UnsetGithubInstallationID\x12).account.UnsetGithubInstallationIDRequest\x1a*.account.UnsetGithubInstallationIDResponse\x12o\n" + + "\x18GetOrCreateAWSExternalId\x12(.account.GetOrCreateAWSExternalIdRequest\x1a).account.GetOrCreateAWSExternalIdResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_account_proto_rawDescOnce sync.Once @@ -4407,7 +4491,7 @@ func file_account_proto_rawDescGZIP() []byte { } var file_account_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_account_proto_msgTypes = make([]protoimpl.MessageInfo, 72) +var file_account_proto_msgTypes = make([]protoimpl.MessageInfo, 74) var file_account_proto_goTypes = []any{ (SourceStatus)(0), // 0: account.SourceStatus (RepositoryStatus)(0), // 1: account.RepositoryStatus @@ -4480,16 +4564,18 @@ var file_account_proto_goTypes = []any{ (*SetGithubInstallationIDResponse)(nil), // 68: account.SetGithubInstallationIDResponse (*UnsetGithubInstallationIDRequest)(nil), // 69: account.UnsetGithubInstallationIDRequest (*UnsetGithubInstallationIDResponse)(nil), // 70: account.UnsetGithubInstallationIDResponse - (*ListTeamMembersRequest)(nil), // 71: account.ListTeamMembersRequest - (*ListTeamMembersResponse)(nil), // 72: account.ListTeamMembersResponse - (*TeamMember)(nil), // 73: account.TeamMember - (*GetWelcomeScreenInformationRequest)(nil), // 74: account.GetWelcomeScreenInformationRequest - (*InviterInformation)(nil), // 75: account.InviterInformation - (*GetWelcomeScreenInformationResponse)(nil), // 76: account.GetWelcomeScreenInformationResponse - (*timestamppb.Timestamp)(nil), // 77: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 78: google.protobuf.Struct - (*durationpb.Duration)(nil), // 79: google.protobuf.Duration - (QueryMethod)(0), // 80: QueryMethod + (*GetOrCreateAWSExternalIdRequest)(nil), // 71: account.GetOrCreateAWSExternalIdRequest + (*GetOrCreateAWSExternalIdResponse)(nil), // 72: account.GetOrCreateAWSExternalIdResponse + (*ListTeamMembersRequest)(nil), // 73: account.ListTeamMembersRequest + (*ListTeamMembersResponse)(nil), // 74: account.ListTeamMembersResponse + (*TeamMember)(nil), // 75: account.TeamMember + (*GetWelcomeScreenInformationRequest)(nil), // 76: account.GetWelcomeScreenInformationRequest + (*InviterInformation)(nil), // 77: account.InviterInformation + (*GetWelcomeScreenInformationResponse)(nil), // 78: account.GetWelcomeScreenInformationResponse + (*timestamppb.Timestamp)(nil), // 79: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 80: google.protobuf.Struct + (*durationpb.Duration)(nil), // 81: google.protobuf.Duration + (QueryMethod)(0), // 82: QueryMethod } var file_account_proto_depIdxs = []int32{ 25, // 0: account.ListAccountsResponse.accounts:type_name -> account.Account @@ -4507,15 +4593,15 @@ var file_account_proto_depIdxs = []int32{ 54, // 12: account.AdminCreateTokenRequest.request:type_name -> account.CreateTokenRequest 23, // 13: account.Source.metadata:type_name -> account.SourceMetadata 24, // 14: account.Source.properties:type_name -> account.SourceProperties - 77, // 15: account.SourceMetadata.TokenExpiry:type_name -> google.protobuf.Timestamp + 79, // 15: account.SourceMetadata.TokenExpiry:type_name -> google.protobuf.Timestamp 0, // 16: account.SourceMetadata.Status:type_name -> account.SourceStatus - 78, // 17: account.SourceProperties.Config:type_name -> google.protobuf.Struct - 78, // 18: account.SourceProperties.AdditionalConfig:type_name -> google.protobuf.Struct + 80, // 17: account.SourceProperties.Config:type_name -> google.protobuf.Struct + 80, // 18: account.SourceProperties.AdditionalConfig:type_name -> google.protobuf.Struct 26, // 19: account.Account.metadata:type_name -> account.AccountMetadata 28, // 20: account.Account.properties:type_name -> account.AccountProperties 27, // 21: account.AccountMetadata.repositories:type_name -> account.Repository 2, // 22: account.AccountMetadata.Plan:type_name -> account.AccountPlan - 77, // 23: account.Repository.lastChangeAt:type_name -> google.protobuf.Timestamp + 79, // 23: account.Repository.lastChangeAt:type_name -> google.protobuf.Timestamp 1, // 24: account.Repository.status:type_name -> account.RepositoryStatus 25, // 25: account.GetAccountResponse.account:type_name -> account.Account 22, // 26: account.ListSourcesResponse.Sources:type_name -> account.Source @@ -4526,27 +4612,27 @@ var file_account_proto_depIdxs = []int32{ 22, // 31: account.UpdateSourceResponse.source:type_name -> account.Source 0, // 32: account.SourceKeepaliveResult.Status:type_name -> account.SourceStatus 0, // 33: account.SourceHealth.status:type_name -> account.SourceStatus - 77, // 34: account.SourceHealth.createdAt:type_name -> google.protobuf.Timestamp - 77, // 35: account.SourceHealth.lastHeartbeat:type_name -> google.protobuf.Timestamp - 77, // 36: account.SourceHealth.nextHeartbeat:type_name -> google.protobuf.Timestamp + 79, // 34: account.SourceHealth.createdAt:type_name -> google.protobuf.Timestamp + 79, // 35: account.SourceHealth.lastHeartbeat:type_name -> google.protobuf.Timestamp + 79, // 36: account.SourceHealth.nextHeartbeat:type_name -> google.protobuf.Timestamp 3, // 37: account.SourceHealth.managed:type_name -> account.SourceManaged 48, // 38: account.SourceHealth.adapterMetadata:type_name -> account.AdapterMetadata 45, // 39: account.ListAllSourcesStatusResponse.sources:type_name -> account.SourceHealth - 79, // 40: account.SubmitSourceHeartbeatRequest.nextHeartbeatMax:type_name -> google.protobuf.Duration + 81, // 40: account.SubmitSourceHeartbeatRequest.nextHeartbeatMax:type_name -> google.protobuf.Duration 3, // 41: account.SubmitSourceHeartbeatRequest.managed:type_name -> account.SourceManaged 48, // 42: account.SubmitSourceHeartbeatRequest.adapterMetadata:type_name -> account.AdapterMetadata 4, // 43: account.AdapterMetadata.category:type_name -> account.AdapterCategory 49, // 44: account.AdapterMetadata.supportedQueryMethods:type_name -> account.AdapterSupportedQueryMethods 50, // 45: account.AdapterMetadata.terraformMappings:type_name -> account.TerraformMapping - 80, // 46: account.TerraformMapping.terraformMethod:type_name -> QueryMethod - 79, // 47: account.KeepaliveSourcesRequest.timeout:type_name -> google.protobuf.Duration + 82, // 46: account.TerraformMapping.terraformMethod:type_name -> QueryMethod + 81, // 47: account.KeepaliveSourcesRequest.timeout:type_name -> google.protobuf.Duration 43, // 48: account.KeepaliveSourcesResponse.sources:type_name -> account.SourceKeepaliveResult 4, // 49: account.AvailableItemType.category:type_name -> account.AdapterCategory 49, // 50: account.AvailableItemType.supportedQueryMethods:type_name -> account.AdapterSupportedQueryMethods 58, // 51: account.ListAvailableItemTypesResponse.types:type_name -> account.AvailableItemType 45, // 52: account.GetSourceStatusResponse.source:type_name -> account.SourceHealth - 73, // 53: account.ListTeamMembersResponse.members:type_name -> account.TeamMember - 75, // 54: account.GetWelcomeScreenInformationResponse.inviter_information:type_name -> account.InviterInformation + 75, // 53: account.ListTeamMembersResponse.members:type_name -> account.TeamMember + 77, // 54: account.GetWelcomeScreenInformationResponse.inviter_information:type_name -> account.InviterInformation 5, // 55: account.AdminService.ListAccounts:input_type -> account.ListAccountsRequest 7, // 56: account.AdminService.CreateAccount:input_type -> account.CreateAccountRequest 11, // 57: account.AdminService.UpdateAccount:input_type -> account.AdminUpdateAccountRequest @@ -4576,45 +4662,47 @@ var file_account_proto_depIdxs = []int32{ 61, // 81: account.ManagementService.GetSourceStatus:input_type -> account.GetSourceStatusRequest 63, // 82: account.ManagementService.GetUserOnboardingStatus:input_type -> account.GetUserOnboardingStatusRequest 65, // 83: account.ManagementService.SetUserOnboardingStatus:input_type -> account.SetUserOnboardingStatusRequest - 71, // 84: account.ManagementService.ListTeamMembers:input_type -> account.ListTeamMembersRequest - 74, // 85: account.ManagementService.GetWelcomeScreenInformation:input_type -> account.GetWelcomeScreenInformationRequest + 73, // 84: account.ManagementService.ListTeamMembers:input_type -> account.ListTeamMembersRequest + 76, // 85: account.ManagementService.GetWelcomeScreenInformation:input_type -> account.GetWelcomeScreenInformationRequest 67, // 86: account.ManagementService.SetGithubInstallationID:input_type -> account.SetGithubInstallationIDRequest 69, // 87: account.ManagementService.UnsetGithubInstallationID:input_type -> account.UnsetGithubInstallationIDRequest - 6, // 88: account.AdminService.ListAccounts:output_type -> account.ListAccountsResponse - 8, // 89: account.AdminService.CreateAccount:output_type -> account.CreateAccountResponse - 10, // 90: account.AdminService.UpdateAccount:output_type -> account.UpdateAccountResponse - 30, // 91: account.AdminService.GetAccount:output_type -> account.GetAccountResponse - 14, // 92: account.AdminService.DeleteAccount:output_type -> account.AdminDeleteAccountResponse - 34, // 93: account.AdminService.ListSources:output_type -> account.ListSourcesResponse - 36, // 94: account.AdminService.CreateSource:output_type -> account.CreateSourceResponse - 38, // 95: account.AdminService.GetSource:output_type -> account.GetSourceResponse - 40, // 96: account.AdminService.UpdateSource:output_type -> account.UpdateSourceResponse - 42, // 97: account.AdminService.DeleteSource:output_type -> account.DeleteSourceResponse - 53, // 98: account.AdminService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse - 55, // 99: account.AdminService.CreateToken:output_type -> account.CreateTokenResponse - 30, // 100: account.ManagementService.GetAccount:output_type -> account.GetAccountResponse - 32, // 101: account.ManagementService.DeleteAccount:output_type -> account.DeleteAccountResponse - 34, // 102: account.ManagementService.ListSources:output_type -> account.ListSourcesResponse - 36, // 103: account.ManagementService.CreateSource:output_type -> account.CreateSourceResponse - 38, // 104: account.ManagementService.GetSource:output_type -> account.GetSourceResponse - 40, // 105: account.ManagementService.UpdateSource:output_type -> account.UpdateSourceResponse - 42, // 106: account.ManagementService.DeleteSource:output_type -> account.DeleteSourceResponse - 46, // 107: account.ManagementService.ListAllSourcesStatus:output_type -> account.ListAllSourcesStatusResponse - 46, // 108: account.ManagementService.ListActiveSourcesStatus:output_type -> account.ListAllSourcesStatusResponse - 51, // 109: account.ManagementService.SubmitSourceHeartbeat:output_type -> account.SubmitSourceHeartbeatResponse - 53, // 110: account.ManagementService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse - 55, // 111: account.ManagementService.CreateToken:output_type -> account.CreateTokenResponse - 57, // 112: account.ManagementService.RevlinkWarmup:output_type -> account.RevlinkWarmupResponse - 60, // 113: account.ManagementService.ListAvailableItemTypes:output_type -> account.ListAvailableItemTypesResponse - 62, // 114: account.ManagementService.GetSourceStatus:output_type -> account.GetSourceStatusResponse - 64, // 115: account.ManagementService.GetUserOnboardingStatus:output_type -> account.GetUserOnboardingStatusResponse - 66, // 116: account.ManagementService.SetUserOnboardingStatus:output_type -> account.SetUserOnboardingStatusResponse - 72, // 117: account.ManagementService.ListTeamMembers:output_type -> account.ListTeamMembersResponse - 76, // 118: account.ManagementService.GetWelcomeScreenInformation:output_type -> account.GetWelcomeScreenInformationResponse - 68, // 119: account.ManagementService.SetGithubInstallationID:output_type -> account.SetGithubInstallationIDResponse - 70, // 120: account.ManagementService.UnsetGithubInstallationID:output_type -> account.UnsetGithubInstallationIDResponse - 88, // [88:121] is the sub-list for method output_type - 55, // [55:88] is the sub-list for method input_type + 71, // 88: account.ManagementService.GetOrCreateAWSExternalId:input_type -> account.GetOrCreateAWSExternalIdRequest + 6, // 89: account.AdminService.ListAccounts:output_type -> account.ListAccountsResponse + 8, // 90: account.AdminService.CreateAccount:output_type -> account.CreateAccountResponse + 10, // 91: account.AdminService.UpdateAccount:output_type -> account.UpdateAccountResponse + 30, // 92: account.AdminService.GetAccount:output_type -> account.GetAccountResponse + 14, // 93: account.AdminService.DeleteAccount:output_type -> account.AdminDeleteAccountResponse + 34, // 94: account.AdminService.ListSources:output_type -> account.ListSourcesResponse + 36, // 95: account.AdminService.CreateSource:output_type -> account.CreateSourceResponse + 38, // 96: account.AdminService.GetSource:output_type -> account.GetSourceResponse + 40, // 97: account.AdminService.UpdateSource:output_type -> account.UpdateSourceResponse + 42, // 98: account.AdminService.DeleteSource:output_type -> account.DeleteSourceResponse + 53, // 99: account.AdminService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse + 55, // 100: account.AdminService.CreateToken:output_type -> account.CreateTokenResponse + 30, // 101: account.ManagementService.GetAccount:output_type -> account.GetAccountResponse + 32, // 102: account.ManagementService.DeleteAccount:output_type -> account.DeleteAccountResponse + 34, // 103: account.ManagementService.ListSources:output_type -> account.ListSourcesResponse + 36, // 104: account.ManagementService.CreateSource:output_type -> account.CreateSourceResponse + 38, // 105: account.ManagementService.GetSource:output_type -> account.GetSourceResponse + 40, // 106: account.ManagementService.UpdateSource:output_type -> account.UpdateSourceResponse + 42, // 107: account.ManagementService.DeleteSource:output_type -> account.DeleteSourceResponse + 46, // 108: account.ManagementService.ListAllSourcesStatus:output_type -> account.ListAllSourcesStatusResponse + 46, // 109: account.ManagementService.ListActiveSourcesStatus:output_type -> account.ListAllSourcesStatusResponse + 51, // 110: account.ManagementService.SubmitSourceHeartbeat:output_type -> account.SubmitSourceHeartbeatResponse + 53, // 111: account.ManagementService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse + 55, // 112: account.ManagementService.CreateToken:output_type -> account.CreateTokenResponse + 57, // 113: account.ManagementService.RevlinkWarmup:output_type -> account.RevlinkWarmupResponse + 60, // 114: account.ManagementService.ListAvailableItemTypes:output_type -> account.ListAvailableItemTypesResponse + 62, // 115: account.ManagementService.GetSourceStatus:output_type -> account.GetSourceStatusResponse + 64, // 116: account.ManagementService.GetUserOnboardingStatus:output_type -> account.GetUserOnboardingStatusResponse + 66, // 117: account.ManagementService.SetUserOnboardingStatus:output_type -> account.SetUserOnboardingStatusResponse + 74, // 118: account.ManagementService.ListTeamMembers:output_type -> account.ListTeamMembersResponse + 78, // 119: account.ManagementService.GetWelcomeScreenInformation:output_type -> account.GetWelcomeScreenInformationResponse + 68, // 120: account.ManagementService.SetGithubInstallationID:output_type -> account.SetGithubInstallationIDResponse + 70, // 121: account.ManagementService.UnsetGithubInstallationID:output_type -> account.UnsetGithubInstallationIDResponse + 72, // 122: account.ManagementService.GetOrCreateAWSExternalId:output_type -> account.GetOrCreateAWSExternalIdResponse + 89, // [89:123] is the sub-list for method output_type + 55, // [55:89] is the sub-list for method input_type 55, // [55:55] is the sub-list for extension type_name 55, // [55:55] is the sub-list for extension extendee 0, // [0:55] is the sub-list for field type_name @@ -4634,7 +4722,7 @@ func file_account_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_account_proto_rawDesc), len(file_account_proto_rawDesc)), NumEnums: 5, - NumMessages: 72, + NumMessages: 74, NumExtensions: 0, NumServices: 2, }, diff --git a/sdp-go/apikey.go b/go/sdp-go/apikey.go similarity index 100% rename from sdp-go/apikey.go rename to go/sdp-go/apikey.go diff --git a/sdp-go/apikeys.pb.go b/go/sdp-go/apikeys.pb.go similarity index 99% rename from sdp-go/apikeys.pb.go rename to go/sdp-go/apikeys.pb.go index d303f4b3..8b03f283 100644 --- a/sdp-go/apikeys.pb.go +++ b/go/sdp-go/apikeys.pb.go @@ -1015,7 +1015,7 @@ const file_apikeys_proto_rawDesc = "" + "\fUpdateAPIKey\x12\x1c.apikeys.UpdateAPIKeyRequest\x1a\x1d.apikeys.UpdateAPIKeyResponse\x12H\n" + "\vListAPIKeys\x12\x1b.apikeys.ListAPIKeysRequest\x1a\x1c.apikeys.ListAPIKeysResponse\x12K\n" + "\fDeleteAPIKey\x12\x1c.apikeys.DeleteAPIKeyRequest\x1a\x1d.apikeys.DeleteAPIKeyResponse\x12`\n" + - "\x13ExchangeKeyForToken\x12#.apikeys.ExchangeKeyForTokenRequest\x1a$.apikeys.ExchangeKeyForTokenResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x13ExchangeKeyForToken\x12#.apikeys.ExchangeKeyForTokenRequest\x1a$.apikeys.ExchangeKeyForTokenResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_apikeys_proto_rawDescOnce sync.Once diff --git a/sdp-go/area51.pb.go b/go/sdp-go/area51.pb.go similarity index 99% rename from sdp-go/area51.pb.go rename to go/sdp-go/area51.pb.go index 6faa494d..df1d3c5c 100644 --- a/sdp-go/area51.pb.go +++ b/go/sdp-go/area51.pb.go @@ -256,7 +256,7 @@ const file_area51_proto_rawDesc = "" + "\x18GetChangeArchiveResponse\x12;\n" + "\rchangeArchive\x18\x01 \x01(\v2\x15.area51.ChangeArchiveR\rchangeArchive2f\n" + "\rArea51Service\x12U\n" + - "\x10GetChangeArchive\x12\x1f.area51.GetChangeArchiveRequest\x1a .area51.GetChangeArchiveResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x10GetChangeArchive\x12\x1f.area51.GetChangeArchiveRequest\x1a .area51.GetChangeArchiveResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_area51_proto_rawDescOnce sync.Once diff --git a/sdp-go/auth0support.pb.go b/go/sdp-go/auth0support.pb.go similarity index 98% rename from sdp-go/auth0support.pb.go rename to go/sdp-go/auth0support.pb.go index 0d19e78e..8916ed84 100644 --- a/sdp-go/auth0support.pb.go +++ b/go/sdp-go/auth0support.pb.go @@ -199,7 +199,7 @@ const file_auth0support_proto_rawDesc = "" + "\fAuth0Support\x12Y\n" + "\n" + "CreateUser\x12$.auth0support.Auth0CreateUserRequest\x1a%.auth0support.Auth0CreateUserResponse\x12\\\n" + - "\x10KeepaliveSources\x12%.account.AdminKeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x10KeepaliveSources\x12%.account.AdminKeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_auth0support_proto_rawDescOnce sync.Once diff --git a/sdp-go/bookmarks.go b/go/sdp-go/bookmarks.go similarity index 100% rename from sdp-go/bookmarks.go rename to go/sdp-go/bookmarks.go diff --git a/sdp-go/bookmarks.pb.go b/go/sdp-go/bookmarks.pb.go similarity index 99% rename from sdp-go/bookmarks.pb.go rename to go/sdp-go/bookmarks.pb.go index d47f043d..4e7ca90e 100644 --- a/sdp-go/bookmarks.pb.go +++ b/go/sdp-go/bookmarks.pb.go @@ -796,7 +796,7 @@ const file_bookmarks_proto_rawDesc = "" + "\vGetBookmark\x12\x1d.bookmarks.GetBookmarkRequest\x1a\x1e.bookmarks.GetBookmarkResponse\x12U\n" + "\x0eUpdateBookmark\x12 .bookmarks.UpdateBookmarkRequest\x1a!.bookmarks.UpdateBookmarkResponse\x12U\n" + "\x0eDeleteBookmark\x12 .bookmarks.DeleteBookmarkRequest\x1a!.bookmarks.DeleteBookmarkResponse\x12g\n" + - "\x14GetAffectedBookmarks\x12&.bookmarks.GetAffectedBookmarksRequest\x1a'.bookmarks.GetAffectedBookmarksResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x14GetAffectedBookmarks\x12&.bookmarks.GetAffectedBookmarksRequest\x1a'.bookmarks.GetAffectedBookmarksResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_bookmarks_proto_rawDescOnce sync.Once diff --git a/sdp-go/cached_entry.pb.go b/go/sdp-go/cached_entry.pb.go similarity index 98% rename from sdp-go/cached_entry.pb.go rename to go/sdp-go/cached_entry.pb.go index b33e6638..c82a8e54 100644 --- a/sdp-go/cached_entry.pb.go +++ b/go/sdp-go/cached_entry.pb.go @@ -130,7 +130,7 @@ const file_cached_entry_proto_rawDesc = "" + "\x16unique_attribute_value\x18\x04 \x01(\tR\x14uniqueAttributeValue\x12$\n" + "\x06method\x18\x05 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + "\x05query\x18\x06 \x01(\tR\x05query\x12\x19\n" + - "\bsst_hash\x18\a \x01(\tR\asstHashB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\bsst_hash\x18\a \x01(\tR\asstHashB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_cached_entry_proto_rawDescOnce sync.Once diff --git a/sdp-go/changes.go b/go/sdp-go/changes.go similarity index 87% rename from sdp-go/changes.go rename to go/sdp-go/changes.go index 63881d87..b338f2b1 100644 --- a/sdp-go/changes.go +++ b/go/sdp-go/changes.go @@ -8,6 +8,7 @@ import ( "gopkg.in/yaml.v3" ) +// GetUUIDParsed returns the parsed UUID from the ChangeMetadata, or nil if invalid. func (a *ChangeMetadata) GetUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(a.GetUUID()) if err != nil { @@ -16,6 +17,7 @@ func (a *ChangeMetadata) GetUUIDParsed() *uuid.UUID { return &u } +// GetNullUUID returns the UUID as a nullable type for database operations. func (a *ChangeMetadata) GetNullUUID() uuid.NullUUID { u := a.GetUUIDParsed() if u == nil { @@ -24,6 +26,8 @@ func (a *ChangeMetadata) GetNullUUID() uuid.NullUUID { return uuid.NullUUID{UUID: *u, Valid: true} } +// GetChangingItemsBookmarkUUIDParsed returns the parsed UUID for the bookmark +// containing the directly affected items, or nil if invalid. func (a *ChangeProperties) GetChangingItemsBookmarkUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(a.GetChangingItemsBookmarkUUID()) if err != nil { @@ -32,6 +36,8 @@ func (a *ChangeProperties) GetChangingItemsBookmarkUUIDParsed() *uuid.UUID { return &u } +// GetSystemBeforeSnapshotUUIDParsed returns the parsed UUID for the whole-system +// snapshot taken before the change, or nil if invalid. func (a *ChangeProperties) GetSystemBeforeSnapshotUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(a.GetSystemBeforeSnapshotUUID()) if err != nil { @@ -40,6 +46,8 @@ func (a *ChangeProperties) GetSystemBeforeSnapshotUUIDParsed() *uuid.UUID { return &u } +// GetSystemAfterSnapshotUUIDParsed returns the parsed UUID for the whole-system +// snapshot taken after the change, or nil if invalid. func (a *ChangeProperties) GetSystemAfterSnapshotUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(a.GetSystemAfterSnapshotUUID()) if err != nil { @@ -48,6 +56,7 @@ func (a *ChangeProperties) GetSystemAfterSnapshotUUIDParsed() *uuid.UUID { return &u } +// GetUUIDParsed returns the parsed UUID from GetChangeRequest, or nil if invalid. func (a *GetChangeRequest) GetUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(a.GetUUID()) if err != nil { @@ -56,6 +65,7 @@ func (a *GetChangeRequest) GetUUIDParsed() *uuid.UUID { return &u } +// GetUUIDParsed returns the parsed UUID from UpdateChangeRequest, or nil if invalid. func (a *UpdateChangeRequest) GetUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(a.GetUUID()) if err != nil { @@ -64,6 +74,7 @@ func (a *UpdateChangeRequest) GetUUIDParsed() *uuid.UUID { return &u } +// GetUUIDParsed returns the parsed UUID from DeleteChangeRequest, or nil if invalid. func (a *DeleteChangeRequest) GetUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(a.GetUUID()) if err != nil { @@ -72,6 +83,7 @@ func (a *DeleteChangeRequest) GetUUIDParsed() *uuid.UUID { return &u } +// GetChangeUUIDParsed returns the parsed change UUID from GetDiffRequest, or nil if invalid. func (x *GetDiffRequest) GetChangeUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(x.GetChangeUUID()) if err != nil { @@ -80,6 +92,7 @@ func (x *GetDiffRequest) GetChangeUUIDParsed() *uuid.UUID { return &u } +// GetChangeUUIDParsed returns the parsed change UUID from ListChangingItemsSummaryRequest, or nil if invalid. func (x *ListChangingItemsSummaryRequest) GetChangeUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(x.GetChangeUUID()) if err != nil { @@ -88,6 +101,7 @@ func (x *ListChangingItemsSummaryRequest) GetChangeUUIDParsed() *uuid.UUID { return &u } +// GetChangeUUIDParsed returns the parsed change UUID from StartChangeRequest, or nil if invalid. func (x *StartChangeRequest) GetChangeUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(x.GetChangeUUID()) if err != nil { @@ -96,6 +110,7 @@ func (x *StartChangeRequest) GetChangeUUIDParsed() *uuid.UUID { return &u } +// GetChangeUUIDParsed returns the parsed change UUID from EndChangeRequest, or nil if invalid. func (x *EndChangeRequest) GetChangeUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(x.GetChangeUUID()) if err != nil { @@ -104,6 +119,7 @@ func (x *EndChangeRequest) GetChangeUUIDParsed() *uuid.UUID { return &u } +// ToMap converts a Change to a map for serialization (e.g., for LLM templates). func (c *Change) ToMap() map[string]any { return map[string]any{ "metadata": c.GetMetadata().ToMap(), @@ -111,6 +127,7 @@ func (c *Change) ToMap() map[string]any { } } +// stringFromUuidBytes converts UUID bytes to a string, returning empty string on error. func stringFromUuidBytes(b []byte) string { u, err := uuid.FromBytes(b) if err != nil { @@ -119,6 +136,7 @@ func stringFromUuidBytes(b []byte) string { return u.String() } +// ToMap converts a Reference to a map for serialization. func (r *Reference) ToMap() map[string]any { return map[string]any{ "type": r.GetType(), @@ -127,6 +145,7 @@ func (r *Reference) ToMap() map[string]any { } } +// ToMap converts a Risk to a map for serialization, including related items. func (r *Risk) ToMap() map[string]any { relatedItems := make([]map[string]any, len(r.GetRelatedItems())) for i, ri := range r.GetRelatedItems() { @@ -142,6 +161,7 @@ func (r *Risk) ToMap() map[string]any { } } +// ToMap converts a GetChangeRisksResponse to a map for serialization. func (r *GetChangeRisksResponse) ToMap() map[string]any { rmd := r.GetChangeRiskMetadata() risks := make([]map[string]any, len(rmd.GetRisks())) @@ -158,6 +178,7 @@ func (r *GetChangeRisksResponse) ToMap() map[string]any { } } +// ToMap converts ChangeMetadata to a map for serialization. func (cm *ChangeMetadata) ToMap() map[string]any { return map[string]any{ "UUID": stringFromUuidBytes(cm.GetUUID()), @@ -179,6 +200,7 @@ func (cm *ChangeMetadata) ToMap() map[string]any { } } +// ToMap converts an Item to a map for serialization. func (i *Item) ToMap() map[string]any { return map[string]any{ "type": i.GetType(), @@ -188,6 +210,7 @@ func (i *Item) ToMap() map[string]any { } } +// ToMap converts an ItemDiff to a map for serialization. func (id *ItemDiff) ToMap() map[string]any { result := map[string]any{ "status": id.GetStatus().String(), @@ -204,6 +227,8 @@ func (id *ItemDiff) ToMap() map[string]any { return result } +// GloballyUniqueName returns the GUN from the item, before, or after state, +// whichever is available (in that order of preference). func (id *ItemDiff) GloballyUniqueName() string { if id.GetItem() != nil { return id.GetItem().GloballyUniqueName() @@ -216,6 +241,7 @@ func (id *ItemDiff) GloballyUniqueName() string { } } +// ToMap converts ChangeProperties to a map for serialization. func (cp *ChangeProperties) ToMap() map[string]any { plannedChanges := make([]map[string]any, len(cp.GetPlannedChanges())) for i, id := range cp.GetPlannedChanges() { @@ -240,6 +266,7 @@ func (cp *ChangeProperties) ToMap() map[string]any { } } +// ToMap converts ChangeAnalysisStatus to a map for serialization. func (rcs *ChangeAnalysisStatus) ToMap() map[string]any { if rcs == nil { return map[string]any{} @@ -250,6 +277,7 @@ func (rcs *ChangeAnalysisStatus) ToMap() map[string]any { } } +// ToMessage converts a StartChangeResponse_State enum to a human-readable message. func (s StartChangeResponse_State) ToMessage() string { switch s { case StartChangeResponse_STATE_UNSPECIFIED: @@ -265,6 +293,7 @@ func (s StartChangeResponse_State) ToMessage() string { } } +// ToMessage converts an EndChangeResponse_State enum to a human-readable message. func (s EndChangeResponse_State) ToMessage() string { switch s { case EndChangeResponse_STATE_UNSPECIFIED: diff --git a/sdp-go/changes.pb.go b/go/sdp-go/changes.pb.go similarity index 90% rename from sdp-go/changes.pb.go rename to go/sdp-go/changes.pb.go index 41245de2..9a4130f2 100644 --- a/sdp-go/changes.pb.go +++ b/go/sdp-go/changes.pb.go @@ -148,6 +148,8 @@ const ( HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_PROVEN HypothesisStatus = 3 // The hypothesis has been disproven, no risk has been found HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN HypothesisStatus = 4 + // The hypothesis was skipped and not investigated + HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED HypothesisStatus = 5 ) // Enum value maps for HypothesisStatus. @@ -158,6 +160,7 @@ var ( 2: "INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING", 3: "INVESTIGATED_HYPOTHESIS_STATUS_PROVEN", 4: "INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN", + 5: "INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED", } HypothesisStatus_value = map[string]int32{ "INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED": 0, @@ -165,6 +168,7 @@ var ( "INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING": 2, "INVESTIGATED_HYPOTHESIS_STATUS_PROVEN": 3, "INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN": 4, + "INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED": 5, } ) @@ -531,7 +535,7 @@ func (x StartChangeResponse_State) Number() protoreflect.EnumNumber { // Deprecated: Use StartChangeResponse_State.Descriptor instead. func (StartChangeResponse_State) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{84, 0} + return file_changes_proto_rawDescGZIP(), []int{86, 0} } type EndChangeResponse_State int32 @@ -587,7 +591,7 @@ func (x EndChangeResponse_State) Number() protoreflect.EnumNumber { // Deprecated: Use EndChangeResponse_State.Descriptor instead. func (EndChangeResponse_State) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{86, 0} + return file_changes_proto_rawDescGZIP(), []int{88, 0} } type Risk_Severity int32 @@ -639,7 +643,7 @@ func (x Risk_Severity) Number() protoreflect.EnumNumber { // Deprecated: Use Risk_Severity.Descriptor instead. func (Risk_Severity) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{89, 0} + return file_changes_proto_rawDescGZIP(), []int{91, 0} } type ChangeAnalysisStatus_Status int32 @@ -694,7 +698,7 @@ func (x ChangeAnalysisStatus_Status) Number() protoreflect.EnumNumber { // Deprecated: Use ChangeAnalysisStatus_Status.Descriptor instead. func (ChangeAnalysisStatus_Status) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{90, 0} + return file_changes_proto_rawDescGZIP(), []int{92, 0} } type LabelRule struct { @@ -1523,6 +1527,126 @@ func (x *ReapplyLabelRuleInTimeRangeResponse) GetChangeUUID() [][]byte { return nil } +type KnowledgeReference struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + FileName string `protobuf:"bytes,2,opt,name=fileName,proto3" json:"fileName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KnowledgeReference) Reset() { + *x = KnowledgeReference{} + mi := &file_changes_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KnowledgeReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KnowledgeReference) ProtoMessage() {} + +func (x *KnowledgeReference) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KnowledgeReference.ProtoReflect.Descriptor instead. +func (*KnowledgeReference) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{17} +} + +func (x *KnowledgeReference) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *KnowledgeReference) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +type Knowledge struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` + FileName string `protobuf:"bytes,4,opt,name=fileName,proto3" json:"fileName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Knowledge) Reset() { + *x = Knowledge{} + mi := &file_changes_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Knowledge) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Knowledge) ProtoMessage() {} + +func (x *Knowledge) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Knowledge.ProtoReflect.Descriptor instead. +func (*Knowledge) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{18} +} + +func (x *Knowledge) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Knowledge) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Knowledge) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *Knowledge) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + type GetHypothesesDetailsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` @@ -1532,7 +1656,7 @@ type GetHypothesesDetailsRequest struct { func (x *GetHypothesesDetailsRequest) Reset() { *x = GetHypothesesDetailsRequest{} - mi := &file_changes_proto_msgTypes[17] + mi := &file_changes_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1544,7 +1668,7 @@ func (x *GetHypothesesDetailsRequest) String() string { func (*GetHypothesesDetailsRequest) ProtoMessage() {} func (x *GetHypothesesDetailsRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[17] + mi := &file_changes_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1557,7 +1681,7 @@ func (x *GetHypothesesDetailsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetHypothesesDetailsRequest.ProtoReflect.Descriptor instead. func (*GetHypothesesDetailsRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{17} + return file_changes_proto_rawDescGZIP(), []int{19} } func (x *GetHypothesesDetailsRequest) GetChangeUUID() []byte { @@ -1576,7 +1700,7 @@ type GetHypothesesDetailsResponse struct { func (x *GetHypothesesDetailsResponse) Reset() { *x = GetHypothesesDetailsResponse{} - mi := &file_changes_proto_msgTypes[18] + mi := &file_changes_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1588,7 +1712,7 @@ func (x *GetHypothesesDetailsResponse) String() string { func (*GetHypothesesDetailsResponse) ProtoMessage() {} func (x *GetHypothesesDetailsResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[18] + mi := &file_changes_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1601,7 +1725,7 @@ func (x *GetHypothesesDetailsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetHypothesesDetailsResponse.ProtoReflect.Descriptor instead. func (*GetHypothesesDetailsResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{18} + return file_changes_proto_rawDescGZIP(), []int{20} } func (x *GetHypothesesDetailsResponse) GetHypotheses() []*HypothesesDetails { @@ -1623,13 +1747,15 @@ type HypothesesDetails struct { Status HypothesisStatus `protobuf:"varint,4,opt,name=status,proto3,enum=changes.HypothesisStatus" json:"status,omitempty"` // The results of the investigation of the hypothesis InvestigationResults string `protobuf:"bytes,5,opt,name=investigationResults,proto3" json:"investigationResults,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Knowledge used when investigating this hypothesis + KnowledgeUsed []*KnowledgeReference `protobuf:"bytes,6,rep,name=knowledgeUsed,proto3" json:"knowledgeUsed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HypothesesDetails) Reset() { *x = HypothesesDetails{} - mi := &file_changes_proto_msgTypes[19] + mi := &file_changes_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1641,7 +1767,7 @@ func (x *HypothesesDetails) String() string { func (*HypothesesDetails) ProtoMessage() {} func (x *HypothesesDetails) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[19] + mi := &file_changes_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1654,7 +1780,7 @@ func (x *HypothesesDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use HypothesesDetails.ProtoReflect.Descriptor instead. func (*HypothesesDetails) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{19} + return file_changes_proto_rawDescGZIP(), []int{21} } func (x *HypothesesDetails) GetTitle() string { @@ -1692,6 +1818,13 @@ func (x *HypothesesDetails) GetInvestigationResults() string { return "" } +func (x *HypothesesDetails) GetKnowledgeUsed() []*KnowledgeReference { + if x != nil { + return x.KnowledgeUsed + } + return nil +} + type GetChangeTimelineV2Request struct { state protoimpl.MessageState `protogen:"open.v1"` ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` @@ -1701,7 +1834,7 @@ type GetChangeTimelineV2Request struct { func (x *GetChangeTimelineV2Request) Reset() { *x = GetChangeTimelineV2Request{} - mi := &file_changes_proto_msgTypes[20] + mi := &file_changes_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1713,7 +1846,7 @@ func (x *GetChangeTimelineV2Request) String() string { func (*GetChangeTimelineV2Request) ProtoMessage() {} func (x *GetChangeTimelineV2Request) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[20] + mi := &file_changes_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1726,7 +1859,7 @@ func (x *GetChangeTimelineV2Request) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeTimelineV2Request.ProtoReflect.Descriptor instead. func (*GetChangeTimelineV2Request) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{20} + return file_changes_proto_rawDescGZIP(), []int{22} } func (x *GetChangeTimelineV2Request) GetChangeUUID() []byte { @@ -1746,7 +1879,7 @@ type GetChangeTimelineV2Response struct { func (x *GetChangeTimelineV2Response) Reset() { *x = GetChangeTimelineV2Response{} - mi := &file_changes_proto_msgTypes[21] + mi := &file_changes_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1758,7 +1891,7 @@ func (x *GetChangeTimelineV2Response) String() string { func (*GetChangeTimelineV2Response) ProtoMessage() {} func (x *GetChangeTimelineV2Response) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[21] + mi := &file_changes_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1771,7 +1904,7 @@ func (x *GetChangeTimelineV2Response) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeTimelineV2Response.ProtoReflect.Descriptor instead. func (*GetChangeTimelineV2Response) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{21} + return file_changes_proto_rawDescGZIP(), []int{23} } func (x *GetChangeTimelineV2Response) GetEntries() []*ChangeTimelineEntryV2 { @@ -1829,7 +1962,7 @@ type ChangeTimelineEntryV2 struct { func (x *ChangeTimelineEntryV2) Reset() { *x = ChangeTimelineEntryV2{} - mi := &file_changes_proto_msgTypes[22] + mi := &file_changes_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1841,7 +1974,7 @@ func (x *ChangeTimelineEntryV2) String() string { func (*ChangeTimelineEntryV2) ProtoMessage() {} func (x *ChangeTimelineEntryV2) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[22] + mi := &file_changes_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1854,7 +1987,7 @@ func (x *ChangeTimelineEntryV2) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeTimelineEntryV2.ProtoReflect.Descriptor instead. func (*ChangeTimelineEntryV2) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{22} + return file_changes_proto_rawDescGZIP(), []int{24} } func (x *ChangeTimelineEntryV2) GetName() string { @@ -2094,7 +2227,7 @@ type EmptyContent struct { func (x *EmptyContent) Reset() { *x = EmptyContent{} - mi := &file_changes_proto_msgTypes[23] + mi := &file_changes_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2106,7 +2239,7 @@ func (x *EmptyContent) String() string { func (*EmptyContent) ProtoMessage() {} func (x *EmptyContent) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[23] + mi := &file_changes_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2119,7 +2252,7 @@ func (x *EmptyContent) ProtoReflect() protoreflect.Message { // Deprecated: Use EmptyContent.ProtoReflect.Descriptor instead. func (*EmptyContent) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{23} + return file_changes_proto_rawDescGZIP(), []int{25} } // Per-item summary for timeline display - only what the UI needs @@ -2137,7 +2270,7 @@ type MappedItemTimelineSummary struct { func (x *MappedItemTimelineSummary) Reset() { *x = MappedItemTimelineSummary{} - mi := &file_changes_proto_msgTypes[24] + mi := &file_changes_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2149,7 +2282,7 @@ func (x *MappedItemTimelineSummary) String() string { func (*MappedItemTimelineSummary) ProtoMessage() {} func (x *MappedItemTimelineSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[24] + mi := &file_changes_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2162,7 +2295,7 @@ func (x *MappedItemTimelineSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedItemTimelineSummary.ProtoReflect.Descriptor instead. func (*MappedItemTimelineSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{24} + return file_changes_proto_rawDescGZIP(), []int{26} } func (x *MappedItemTimelineSummary) GetDisplayName() string { @@ -2202,7 +2335,7 @@ type MappedItemsTimelineEntry struct { func (x *MappedItemsTimelineEntry) Reset() { *x = MappedItemsTimelineEntry{} - mi := &file_changes_proto_msgTypes[25] + mi := &file_changes_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2214,7 +2347,7 @@ func (x *MappedItemsTimelineEntry) String() string { func (*MappedItemsTimelineEntry) ProtoMessage() {} func (x *MappedItemsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[25] + mi := &file_changes_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2227,7 +2360,7 @@ func (x *MappedItemsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedItemsTimelineEntry.ProtoReflect.Descriptor instead. func (*MappedItemsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{25} + return file_changes_proto_rawDescGZIP(), []int{27} } // Deprecated: Marked as deprecated in changes.proto. @@ -2255,7 +2388,7 @@ type CalculatedBlastRadiusTimelineEntry struct { func (x *CalculatedBlastRadiusTimelineEntry) Reset() { *x = CalculatedBlastRadiusTimelineEntry{} - mi := &file_changes_proto_msgTypes[26] + mi := &file_changes_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2267,7 +2400,7 @@ func (x *CalculatedBlastRadiusTimelineEntry) String() string { func (*CalculatedBlastRadiusTimelineEntry) ProtoMessage() {} func (x *CalculatedBlastRadiusTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[26] + mi := &file_changes_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2280,7 +2413,7 @@ func (x *CalculatedBlastRadiusTimelineEntry) ProtoReflect() protoreflect.Message // Deprecated: Use CalculatedBlastRadiusTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedBlastRadiusTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{26} + return file_changes_proto_rawDescGZIP(), []int{28} } func (x *CalculatedBlastRadiusTimelineEntry) GetNumItems() uint32 { @@ -2308,7 +2441,7 @@ type RecordObservationsTimelineEntry struct { func (x *RecordObservationsTimelineEntry) Reset() { *x = RecordObservationsTimelineEntry{} - mi := &file_changes_proto_msgTypes[27] + mi := &file_changes_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2320,7 +2453,7 @@ func (x *RecordObservationsTimelineEntry) String() string { func (*RecordObservationsTimelineEntry) ProtoMessage() {} func (x *RecordObservationsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[27] + mi := &file_changes_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2333,7 +2466,7 @@ func (x *RecordObservationsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordObservationsTimelineEntry.ProtoReflect.Descriptor instead. func (*RecordObservationsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{27} + return file_changes_proto_rawDescGZIP(), []int{29} } func (x *RecordObservationsTimelineEntry) GetNumObservations() uint32 { @@ -2356,7 +2489,7 @@ type FormHypothesesTimelineEntry struct { func (x *FormHypothesesTimelineEntry) Reset() { *x = FormHypothesesTimelineEntry{} - mi := &file_changes_proto_msgTypes[28] + mi := &file_changes_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2368,7 +2501,7 @@ func (x *FormHypothesesTimelineEntry) String() string { func (*FormHypothesesTimelineEntry) ProtoMessage() {} func (x *FormHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[28] + mi := &file_changes_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2381,7 +2514,7 @@ func (x *FormHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use FormHypothesesTimelineEntry.ProtoReflect.Descriptor instead. func (*FormHypothesesTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{28} + return file_changes_proto_rawDescGZIP(), []int{30} } func (x *FormHypothesesTimelineEntry) GetNumHypotheses() uint32 { @@ -2407,14 +2540,16 @@ type InvestigateHypothesesTimelineEntry struct { // Number of hypotheses that are still being investigated NumInvestigating uint32 `protobuf:"varint,3,opt,name=numInvestigating,proto3" json:"numInvestigating,omitempty"` // The current state of the hypotheses under investigation - Hypotheses []*HypothesisSummary `protobuf:"bytes,4,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + Hypotheses []*HypothesisSummary `protobuf:"bytes,4,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + // Number of hypotheses that were skipped + NumSkipped uint32 `protobuf:"varint,5,opt,name=numSkipped,proto3" json:"numSkipped,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *InvestigateHypothesesTimelineEntry) Reset() { *x = InvestigateHypothesesTimelineEntry{} - mi := &file_changes_proto_msgTypes[29] + mi := &file_changes_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2426,7 +2561,7 @@ func (x *InvestigateHypothesesTimelineEntry) String() string { func (*InvestigateHypothesesTimelineEntry) ProtoMessage() {} func (x *InvestigateHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[29] + mi := &file_changes_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2439,7 +2574,7 @@ func (x *InvestigateHypothesesTimelineEntry) ProtoReflect() protoreflect.Message // Deprecated: Use InvestigateHypothesesTimelineEntry.ProtoReflect.Descriptor instead. func (*InvestigateHypothesesTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{29} + return file_changes_proto_rawDescGZIP(), []int{31} } func (x *InvestigateHypothesesTimelineEntry) GetNumProven() uint32 { @@ -2470,6 +2605,13 @@ func (x *InvestigateHypothesesTimelineEntry) GetHypotheses() []*HypothesisSummar return nil } +func (x *InvestigateHypothesesTimelineEntry) GetNumSkipped() uint32 { + if x != nil { + return x.NumSkipped + } + return 0 +} + type HypothesisSummary struct { state protoimpl.MessageState `protogen:"open.v1"` // The status of the investigation @@ -2488,7 +2630,7 @@ type HypothesisSummary struct { func (x *HypothesisSummary) Reset() { *x = HypothesisSummary{} - mi := &file_changes_proto_msgTypes[30] + mi := &file_changes_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2500,7 +2642,7 @@ func (x *HypothesisSummary) String() string { func (*HypothesisSummary) ProtoMessage() {} func (x *HypothesisSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[30] + mi := &file_changes_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2513,7 +2655,7 @@ func (x *HypothesisSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use HypothesisSummary.ProtoReflect.Descriptor instead. func (*HypothesisSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{30} + return file_changes_proto_rawDescGZIP(), []int{32} } func (x *HypothesisSummary) GetStatus() HypothesisStatus { @@ -2546,7 +2688,7 @@ type CalculatedRisksTimelineEntry struct { func (x *CalculatedRisksTimelineEntry) Reset() { *x = CalculatedRisksTimelineEntry{} - mi := &file_changes_proto_msgTypes[31] + mi := &file_changes_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2558,7 +2700,7 @@ func (x *CalculatedRisksTimelineEntry) String() string { func (*CalculatedRisksTimelineEntry) ProtoMessage() {} func (x *CalculatedRisksTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[31] + mi := &file_changes_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2571,7 +2713,7 @@ func (x *CalculatedRisksTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use CalculatedRisksTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedRisksTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{31} + return file_changes_proto_rawDescGZIP(), []int{33} } func (x *CalculatedRisksTimelineEntry) GetRisks() []*Risk { @@ -2591,7 +2733,7 @@ type CalculatedLabelsTimelineEntry struct { func (x *CalculatedLabelsTimelineEntry) Reset() { *x = CalculatedLabelsTimelineEntry{} - mi := &file_changes_proto_msgTypes[32] + mi := &file_changes_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2603,7 +2745,7 @@ func (x *CalculatedLabelsTimelineEntry) String() string { func (*CalculatedLabelsTimelineEntry) ProtoMessage() {} func (x *CalculatedLabelsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[32] + mi := &file_changes_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2616,7 +2758,7 @@ func (x *CalculatedLabelsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use CalculatedLabelsTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedLabelsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{32} + return file_changes_proto_rawDescGZIP(), []int{34} } func (x *CalculatedLabelsTimelineEntry) GetLabels() []*Label { @@ -2639,7 +2781,7 @@ type ChangeValidationTimelineEntry struct { func (x *ChangeValidationTimelineEntry) Reset() { *x = ChangeValidationTimelineEntry{} - mi := &file_changes_proto_msgTypes[33] + mi := &file_changes_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2651,7 +2793,7 @@ func (x *ChangeValidationTimelineEntry) String() string { func (*ChangeValidationTimelineEntry) ProtoMessage() {} func (x *ChangeValidationTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[33] + mi := &file_changes_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2664,7 +2806,7 @@ func (x *ChangeValidationTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeValidationTimelineEntry.ProtoReflect.Descriptor instead. func (*ChangeValidationTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{33} + return file_changes_proto_rawDescGZIP(), []int{35} } func (x *ChangeValidationTimelineEntry) GetBriefAnalysis() string { @@ -2696,7 +2838,7 @@ type ChangeValidationCategory struct { func (x *ChangeValidationCategory) Reset() { *x = ChangeValidationCategory{} - mi := &file_changes_proto_msgTypes[34] + mi := &file_changes_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2708,7 +2850,7 @@ func (x *ChangeValidationCategory) String() string { func (*ChangeValidationCategory) ProtoMessage() {} func (x *ChangeValidationCategory) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[34] + mi := &file_changes_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2721,7 +2863,7 @@ func (x *ChangeValidationCategory) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeValidationCategory.ProtoReflect.Descriptor instead. func (*ChangeValidationCategory) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{34} + return file_changes_proto_rawDescGZIP(), []int{36} } func (x *ChangeValidationCategory) GetTitle() string { @@ -2747,7 +2889,7 @@ type GetDiffRequest struct { func (x *GetDiffRequest) Reset() { *x = GetDiffRequest{} - mi := &file_changes_proto_msgTypes[35] + mi := &file_changes_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2759,7 +2901,7 @@ func (x *GetDiffRequest) String() string { func (*GetDiffRequest) ProtoMessage() {} func (x *GetDiffRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[35] + mi := &file_changes_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2772,7 +2914,7 @@ func (x *GetDiffRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiffRequest.ProtoReflect.Descriptor instead. func (*GetDiffRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{35} + return file_changes_proto_rawDescGZIP(), []int{37} } func (x *GetDiffRequest) GetChangeUUID() []byte { @@ -2797,7 +2939,7 @@ type GetDiffResponse struct { func (x *GetDiffResponse) Reset() { *x = GetDiffResponse{} - mi := &file_changes_proto_msgTypes[36] + mi := &file_changes_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2809,7 +2951,7 @@ func (x *GetDiffResponse) String() string { func (*GetDiffResponse) ProtoMessage() {} func (x *GetDiffResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[36] + mi := &file_changes_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2822,7 +2964,7 @@ func (x *GetDiffResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiffResponse.ProtoReflect.Descriptor instead. func (*GetDiffResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{36} + return file_changes_proto_rawDescGZIP(), []int{38} } func (x *GetDiffResponse) GetExpectedItems() []*ItemDiff { @@ -2862,7 +3004,7 @@ type ListChangingItemsSummaryRequest struct { func (x *ListChangingItemsSummaryRequest) Reset() { *x = ListChangingItemsSummaryRequest{} - mi := &file_changes_proto_msgTypes[37] + mi := &file_changes_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2874,7 +3016,7 @@ func (x *ListChangingItemsSummaryRequest) String() string { func (*ListChangingItemsSummaryRequest) ProtoMessage() {} func (x *ListChangingItemsSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[37] + mi := &file_changes_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2887,7 +3029,7 @@ func (x *ListChangingItemsSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangingItemsSummaryRequest.ProtoReflect.Descriptor instead. func (*ListChangingItemsSummaryRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{37} + return file_changes_proto_rawDescGZIP(), []int{39} } func (x *ListChangingItemsSummaryRequest) GetChangeUUID() []byte { @@ -2906,7 +3048,7 @@ type ListChangingItemsSummaryResponse struct { func (x *ListChangingItemsSummaryResponse) Reset() { *x = ListChangingItemsSummaryResponse{} - mi := &file_changes_proto_msgTypes[38] + mi := &file_changes_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2918,7 +3060,7 @@ func (x *ListChangingItemsSummaryResponse) String() string { func (*ListChangingItemsSummaryResponse) ProtoMessage() {} func (x *ListChangingItemsSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[38] + mi := &file_changes_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2931,7 +3073,7 @@ func (x *ListChangingItemsSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangingItemsSummaryResponse.ProtoReflect.Descriptor instead. func (*ListChangingItemsSummaryResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{38} + return file_changes_proto_rawDescGZIP(), []int{40} } func (x *ListChangingItemsSummaryResponse) GetItems() []*ItemDiffSummary { @@ -2961,7 +3103,7 @@ type MappedItemDiff struct { func (x *MappedItemDiff) Reset() { *x = MappedItemDiff{} - mi := &file_changes_proto_msgTypes[39] + mi := &file_changes_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2973,7 +3115,7 @@ func (x *MappedItemDiff) String() string { func (*MappedItemDiff) ProtoMessage() {} func (x *MappedItemDiff) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[39] + mi := &file_changes_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2986,7 +3128,7 @@ func (x *MappedItemDiff) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedItemDiff.ProtoReflect.Descriptor instead. func (*MappedItemDiff) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{39} + return file_changes_proto_rawDescGZIP(), []int{41} } func (x *MappedItemDiff) GetItem() *ItemDiff { @@ -3033,13 +3175,15 @@ type StartChangeAnalysisRequest struct { RoutineChangesConfigOverride *RoutineChangesConfig `protobuf:"bytes,5,opt,name=routineChangesConfigOverride,proto3,oneof" json:"routineChangesConfigOverride,omitempty"` // github organisation profile to use for this change GithubOrganisationProfileOverride *GithubOrganisationProfile `protobuf:"bytes,6,opt,name=githubOrganisationProfileOverride,proto3,oneof" json:"githubOrganisationProfileOverride,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Knowledge to be used for change analysis + Knowledge []*Knowledge `protobuf:"bytes,7,rep,name=knowledge,proto3" json:"knowledge,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StartChangeAnalysisRequest) Reset() { *x = StartChangeAnalysisRequest{} - mi := &file_changes_proto_msgTypes[40] + mi := &file_changes_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3051,7 +3195,7 @@ func (x *StartChangeAnalysisRequest) String() string { func (*StartChangeAnalysisRequest) ProtoMessage() {} func (x *StartChangeAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[40] + mi := &file_changes_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3064,7 +3208,7 @@ func (x *StartChangeAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeAnalysisRequest.ProtoReflect.Descriptor instead. func (*StartChangeAnalysisRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{40} + return file_changes_proto_rawDescGZIP(), []int{42} } func (x *StartChangeAnalysisRequest) GetChangeUUID() []byte { @@ -3102,6 +3246,13 @@ func (x *StartChangeAnalysisRequest) GetGithubOrganisationProfileOverride() *Git return nil } +func (x *StartChangeAnalysisRequest) GetKnowledge() []*Knowledge { + if x != nil { + return x.Knowledge + } + return nil +} + // StartChangeAnalysisResponse is used to signal that the change analysis has been successfully started // we use HTTP response codes to signal errors type StartChangeAnalysisResponse struct { @@ -3112,7 +3263,7 @@ type StartChangeAnalysisResponse struct { func (x *StartChangeAnalysisResponse) Reset() { *x = StartChangeAnalysisResponse{} - mi := &file_changes_proto_msgTypes[41] + mi := &file_changes_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3124,7 +3275,7 @@ func (x *StartChangeAnalysisResponse) String() string { func (*StartChangeAnalysisResponse) ProtoMessage() {} func (x *StartChangeAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[41] + mi := &file_changes_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3137,7 +3288,7 @@ func (x *StartChangeAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeAnalysisResponse.ProtoReflect.Descriptor instead. func (*StartChangeAnalysisResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{41} + return file_changes_proto_rawDescGZIP(), []int{43} } type ListHomeChangesRequest struct { @@ -3150,7 +3301,7 @@ type ListHomeChangesRequest struct { func (x *ListHomeChangesRequest) Reset() { *x = ListHomeChangesRequest{} - mi := &file_changes_proto_msgTypes[42] + mi := &file_changes_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3162,7 +3313,7 @@ func (x *ListHomeChangesRequest) String() string { func (*ListHomeChangesRequest) ProtoMessage() {} func (x *ListHomeChangesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[42] + mi := &file_changes_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3175,7 +3326,7 @@ func (x *ListHomeChangesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHomeChangesRequest.ProtoReflect.Descriptor instead. func (*ListHomeChangesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{42} + return file_changes_proto_rawDescGZIP(), []int{44} } func (x *ListHomeChangesRequest) GetPagination() *PaginationRequest { @@ -3209,7 +3360,7 @@ type ChangeFiltersRequest struct { func (x *ChangeFiltersRequest) Reset() { *x = ChangeFiltersRequest{} - mi := &file_changes_proto_msgTypes[43] + mi := &file_changes_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3221,7 +3372,7 @@ func (x *ChangeFiltersRequest) String() string { func (*ChangeFiltersRequest) ProtoMessage() {} func (x *ChangeFiltersRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[43] + mi := &file_changes_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3234,7 +3385,7 @@ func (x *ChangeFiltersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeFiltersRequest.ProtoReflect.Descriptor instead. func (*ChangeFiltersRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{43} + return file_changes_proto_rawDescGZIP(), []int{45} } func (x *ChangeFiltersRequest) GetRepos() []string { @@ -3289,7 +3440,7 @@ type ListHomeChangesResponse struct { func (x *ListHomeChangesResponse) Reset() { *x = ListHomeChangesResponse{} - mi := &file_changes_proto_msgTypes[44] + mi := &file_changes_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3301,7 +3452,7 @@ func (x *ListHomeChangesResponse) String() string { func (*ListHomeChangesResponse) ProtoMessage() {} func (x *ListHomeChangesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[44] + mi := &file_changes_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3314,7 +3465,7 @@ func (x *ListHomeChangesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHomeChangesResponse.ProtoReflect.Descriptor instead. func (*ListHomeChangesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{44} + return file_changes_proto_rawDescGZIP(), []int{46} } func (x *ListHomeChangesResponse) GetChanges() []*ChangeSummary { @@ -3339,7 +3490,7 @@ type PopulateChangeFiltersRequest struct { func (x *PopulateChangeFiltersRequest) Reset() { *x = PopulateChangeFiltersRequest{} - mi := &file_changes_proto_msgTypes[45] + mi := &file_changes_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3351,7 +3502,7 @@ func (x *PopulateChangeFiltersRequest) String() string { func (*PopulateChangeFiltersRequest) ProtoMessage() {} func (x *PopulateChangeFiltersRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[45] + mi := &file_changes_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3364,7 +3515,7 @@ func (x *PopulateChangeFiltersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PopulateChangeFiltersRequest.ProtoReflect.Descriptor instead. func (*PopulateChangeFiltersRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{45} + return file_changes_proto_rawDescGZIP(), []int{47} } type PopulateChangeFiltersResponse struct { @@ -3377,7 +3528,7 @@ type PopulateChangeFiltersResponse struct { func (x *PopulateChangeFiltersResponse) Reset() { *x = PopulateChangeFiltersResponse{} - mi := &file_changes_proto_msgTypes[46] + mi := &file_changes_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3389,7 +3540,7 @@ func (x *PopulateChangeFiltersResponse) String() string { func (*PopulateChangeFiltersResponse) ProtoMessage() {} func (x *PopulateChangeFiltersResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[46] + mi := &file_changes_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3402,7 +3553,7 @@ func (x *PopulateChangeFiltersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PopulateChangeFiltersResponse.ProtoReflect.Descriptor instead. func (*PopulateChangeFiltersResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{46} + return file_changes_proto_rawDescGZIP(), []int{48} } func (x *PopulateChangeFiltersResponse) GetRepos() []string { @@ -3433,7 +3584,7 @@ type ItemDiffSummary struct { func (x *ItemDiffSummary) Reset() { *x = ItemDiffSummary{} - mi := &file_changes_proto_msgTypes[47] + mi := &file_changes_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3445,7 +3596,7 @@ func (x *ItemDiffSummary) String() string { func (*ItemDiffSummary) ProtoMessage() {} func (x *ItemDiffSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[47] + mi := &file_changes_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3458,7 +3609,7 @@ func (x *ItemDiffSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use ItemDiffSummary.ProtoReflect.Descriptor instead. func (*ItemDiffSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{47} + return file_changes_proto_rawDescGZIP(), []int{49} } func (x *ItemDiffSummary) GetItem() *Reference { @@ -3500,7 +3651,7 @@ type ItemDiff struct { func (x *ItemDiff) Reset() { *x = ItemDiff{} - mi := &file_changes_proto_msgTypes[48] + mi := &file_changes_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3512,7 +3663,7 @@ func (x *ItemDiff) String() string { func (*ItemDiff) ProtoMessage() {} func (x *ItemDiff) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[48] + mi := &file_changes_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3525,7 +3676,7 @@ func (x *ItemDiff) ProtoReflect() protoreflect.Message { // Deprecated: Use ItemDiff.ProtoReflect.Descriptor instead. func (*ItemDiff) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{48} + return file_changes_proto_rawDescGZIP(), []int{50} } func (x *ItemDiff) GetItem() *Reference { @@ -3572,7 +3723,7 @@ type EnrichedTags struct { func (x *EnrichedTags) Reset() { *x = EnrichedTags{} - mi := &file_changes_proto_msgTypes[49] + mi := &file_changes_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3584,7 +3735,7 @@ func (x *EnrichedTags) String() string { func (*EnrichedTags) ProtoMessage() {} func (x *EnrichedTags) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[49] + mi := &file_changes_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3597,7 +3748,7 @@ func (x *EnrichedTags) ProtoReflect() protoreflect.Message { // Deprecated: Use EnrichedTags.ProtoReflect.Descriptor instead. func (*EnrichedTags) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{49} + return file_changes_proto_rawDescGZIP(), []int{51} } func (x *EnrichedTags) GetTagValue() map[string]*TagValue { @@ -3622,7 +3773,7 @@ type TagValue struct { func (x *TagValue) Reset() { *x = TagValue{} - mi := &file_changes_proto_msgTypes[50] + mi := &file_changes_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3634,7 +3785,7 @@ func (x *TagValue) String() string { func (*TagValue) ProtoMessage() {} func (x *TagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[50] + mi := &file_changes_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3647,7 +3798,7 @@ func (x *TagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use TagValue.ProtoReflect.Descriptor instead. func (*TagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{50} + return file_changes_proto_rawDescGZIP(), []int{52} } func (x *TagValue) GetValue() isTagValue_Value { @@ -3701,7 +3852,7 @@ type UserTagValue struct { func (x *UserTagValue) Reset() { *x = UserTagValue{} - mi := &file_changes_proto_msgTypes[51] + mi := &file_changes_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3713,7 +3864,7 @@ func (x *UserTagValue) String() string { func (*UserTagValue) ProtoMessage() {} func (x *UserTagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[51] + mi := &file_changes_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3726,7 +3877,7 @@ func (x *UserTagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use UserTagValue.ProtoReflect.Descriptor instead. func (*UserTagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{51} + return file_changes_proto_rawDescGZIP(), []int{53} } func (x *UserTagValue) GetValue() string { @@ -3748,7 +3899,7 @@ type AutoTagValue struct { func (x *AutoTagValue) Reset() { *x = AutoTagValue{} - mi := &file_changes_proto_msgTypes[52] + mi := &file_changes_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3760,7 +3911,7 @@ func (x *AutoTagValue) String() string { func (*AutoTagValue) ProtoMessage() {} func (x *AutoTagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[52] + mi := &file_changes_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3773,7 +3924,7 @@ func (x *AutoTagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use AutoTagValue.ProtoReflect.Descriptor instead. func (*AutoTagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{52} + return file_changes_proto_rawDescGZIP(), []int{54} } func (x *AutoTagValue) GetValue() string { @@ -3816,7 +3967,7 @@ type Label struct { func (x *Label) Reset() { *x = Label{} - mi := &file_changes_proto_msgTypes[53] + mi := &file_changes_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3828,7 +3979,7 @@ func (x *Label) String() string { func (*Label) ProtoMessage() {} func (x *Label) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[53] + mi := &file_changes_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3841,7 +3992,7 @@ func (x *Label) ProtoReflect() protoreflect.Message { // Deprecated: Use Label.ProtoReflect.Descriptor instead. func (*Label) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{53} + return file_changes_proto_rawDescGZIP(), []int{55} } func (x *Label) GetType() LabelType { @@ -3940,7 +4091,7 @@ type ChangeSummary struct { func (x *ChangeSummary) Reset() { *x = ChangeSummary{} - mi := &file_changes_proto_msgTypes[54] + mi := &file_changes_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3952,7 +4103,7 @@ func (x *ChangeSummary) String() string { func (*ChangeSummary) ProtoMessage() {} func (x *ChangeSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[54] + mi := &file_changes_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3965,7 +4116,7 @@ func (x *ChangeSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeSummary.ProtoReflect.Descriptor instead. func (*ChangeSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{54} + return file_changes_proto_rawDescGZIP(), []int{56} } func (x *ChangeSummary) GetUUID() []byte { @@ -4108,7 +4259,7 @@ type Change struct { func (x *Change) Reset() { *x = Change{} - mi := &file_changes_proto_msgTypes[55] + mi := &file_changes_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4120,7 +4271,7 @@ func (x *Change) String() string { func (*Change) ProtoMessage() {} func (x *Change) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[55] + mi := &file_changes_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4133,7 +4284,7 @@ func (x *Change) ProtoReflect() protoreflect.Message { // Deprecated: Use Change.ProtoReflect.Descriptor instead. func (*Change) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{55} + return file_changes_proto_rawDescGZIP(), []int{57} } func (x *Change) GetMetadata() *ChangeMetadata { @@ -4193,13 +4344,15 @@ type ChangeMetadata struct { // This is null/undefined for legacy changes where observations were not tracked. // This count increments immediately as observations are added, providing fast feedback. TotalObservations *uint32 `protobuf:"varint,21,opt,name=total_observations,json=totalObservations,proto3,oneof" json:"total_observations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Persisted change analysis completion status (single source of truth for GetChange/CLI). + ChangeAnalysisStatus *ChangeAnalysisStatus `protobuf:"bytes,22,opt,name=changeAnalysisStatus,proto3" json:"changeAnalysisStatus,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ChangeMetadata) Reset() { *x = ChangeMetadata{} - mi := &file_changes_proto_msgTypes[56] + mi := &file_changes_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4211,7 +4364,7 @@ func (x *ChangeMetadata) String() string { func (*ChangeMetadata) ProtoMessage() {} func (x *ChangeMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[56] + mi := &file_changes_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4224,7 +4377,7 @@ func (x *ChangeMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeMetadata.ProtoReflect.Descriptor instead. func (*ChangeMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{56} + return file_changes_proto_rawDescGZIP(), []int{58} } func (x *ChangeMetadata) GetUUID() []byte { @@ -4367,6 +4520,13 @@ func (x *ChangeMetadata) GetTotalObservations() uint32 { return 0 } +func (x *ChangeMetadata) GetChangeAnalysisStatus() *ChangeAnalysisStatus { + if x != nil { + return x.ChangeAnalysisStatus + } + return nil +} + // user-supplied properties of this change type ChangeProperties struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4422,7 +4582,7 @@ type ChangeProperties struct { func (x *ChangeProperties) Reset() { *x = ChangeProperties{} - mi := &file_changes_proto_msgTypes[57] + mi := &file_changes_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4434,7 +4594,7 @@ func (x *ChangeProperties) String() string { func (*ChangeProperties) ProtoMessage() {} func (x *ChangeProperties) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[57] + mi := &file_changes_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4447,7 +4607,7 @@ func (x *ChangeProperties) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeProperties.ProtoReflect.Descriptor instead. func (*ChangeProperties) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{57} + return file_changes_proto_rawDescGZIP(), []int{59} } func (x *ChangeProperties) GetTitle() string { @@ -4581,7 +4741,7 @@ type GithubChangeInfo struct { func (x *GithubChangeInfo) Reset() { *x = GithubChangeInfo{} - mi := &file_changes_proto_msgTypes[58] + mi := &file_changes_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4593,7 +4753,7 @@ func (x *GithubChangeInfo) String() string { func (*GithubChangeInfo) ProtoMessage() {} func (x *GithubChangeInfo) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[58] + mi := &file_changes_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4606,7 +4766,7 @@ func (x *GithubChangeInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use GithubChangeInfo.ProtoReflect.Descriptor instead. func (*GithubChangeInfo) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{58} + return file_changes_proto_rawDescGZIP(), []int{60} } func (x *GithubChangeInfo) GetAuthorUsername() string { @@ -4646,7 +4806,7 @@ type ListChangesRequest struct { func (x *ListChangesRequest) Reset() { *x = ListChangesRequest{} - mi := &file_changes_proto_msgTypes[59] + mi := &file_changes_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4658,7 +4818,7 @@ func (x *ListChangesRequest) String() string { func (*ListChangesRequest) ProtoMessage() {} func (x *ListChangesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[59] + mi := &file_changes_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4671,7 +4831,7 @@ func (x *ListChangesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesRequest.ProtoReflect.Descriptor instead. func (*ListChangesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{59} + return file_changes_proto_rawDescGZIP(), []int{61} } type ListChangesResponse struct { @@ -4683,7 +4843,7 @@ type ListChangesResponse struct { func (x *ListChangesResponse) Reset() { *x = ListChangesResponse{} - mi := &file_changes_proto_msgTypes[60] + mi := &file_changes_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4695,7 +4855,7 @@ func (x *ListChangesResponse) String() string { func (*ListChangesResponse) ProtoMessage() {} func (x *ListChangesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[60] + mi := &file_changes_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4708,7 +4868,7 @@ func (x *ListChangesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesResponse.ProtoReflect.Descriptor instead. func (*ListChangesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{60} + return file_changes_proto_rawDescGZIP(), []int{62} } func (x *ListChangesResponse) GetChanges() []*Change { @@ -4728,7 +4888,7 @@ type ListChangesByStatusRequest struct { func (x *ListChangesByStatusRequest) Reset() { *x = ListChangesByStatusRequest{} - mi := &file_changes_proto_msgTypes[61] + mi := &file_changes_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4740,7 +4900,7 @@ func (x *ListChangesByStatusRequest) String() string { func (*ListChangesByStatusRequest) ProtoMessage() {} func (x *ListChangesByStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[61] + mi := &file_changes_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4753,7 +4913,7 @@ func (x *ListChangesByStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesByStatusRequest.ProtoReflect.Descriptor instead. func (*ListChangesByStatusRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{61} + return file_changes_proto_rawDescGZIP(), []int{63} } func (x *ListChangesByStatusRequest) GetStatus() ChangeStatus { @@ -4772,7 +4932,7 @@ type ListChangesByStatusResponse struct { func (x *ListChangesByStatusResponse) Reset() { *x = ListChangesByStatusResponse{} - mi := &file_changes_proto_msgTypes[62] + mi := &file_changes_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4784,7 +4944,7 @@ func (x *ListChangesByStatusResponse) String() string { func (*ListChangesByStatusResponse) ProtoMessage() {} func (x *ListChangesByStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[62] + mi := &file_changes_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4797,7 +4957,7 @@ func (x *ListChangesByStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesByStatusResponse.ProtoReflect.Descriptor instead. func (*ListChangesByStatusResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{62} + return file_changes_proto_rawDescGZIP(), []int{64} } func (x *ListChangesByStatusResponse) GetChanges() []*Change { @@ -4817,7 +4977,7 @@ type CreateChangeRequest struct { func (x *CreateChangeRequest) Reset() { *x = CreateChangeRequest{} - mi := &file_changes_proto_msgTypes[63] + mi := &file_changes_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4829,7 +4989,7 @@ func (x *CreateChangeRequest) String() string { func (*CreateChangeRequest) ProtoMessage() {} func (x *CreateChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[63] + mi := &file_changes_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4842,7 +5002,7 @@ func (x *CreateChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChangeRequest.ProtoReflect.Descriptor instead. func (*CreateChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{63} + return file_changes_proto_rawDescGZIP(), []int{65} } func (x *CreateChangeRequest) GetProperties() *ChangeProperties { @@ -4861,7 +5021,7 @@ type CreateChangeResponse struct { func (x *CreateChangeResponse) Reset() { *x = CreateChangeResponse{} - mi := &file_changes_proto_msgTypes[64] + mi := &file_changes_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4873,7 +5033,7 @@ func (x *CreateChangeResponse) String() string { func (*CreateChangeResponse) ProtoMessage() {} func (x *CreateChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[64] + mi := &file_changes_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4886,7 +5046,7 @@ func (x *CreateChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChangeResponse.ProtoReflect.Descriptor instead. func (*CreateChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{64} + return file_changes_proto_rawDescGZIP(), []int{66} } func (x *CreateChangeResponse) GetChange() *Change { @@ -4911,7 +5071,7 @@ type GetChangeRequest struct { func (x *GetChangeRequest) Reset() { *x = GetChangeRequest{} - mi := &file_changes_proto_msgTypes[65] + mi := &file_changes_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4923,7 +5083,7 @@ func (x *GetChangeRequest) String() string { func (*GetChangeRequest) ProtoMessage() {} func (x *GetChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[65] + mi := &file_changes_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4936,7 +5096,7 @@ func (x *GetChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRequest.ProtoReflect.Descriptor instead. func (*GetChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{65} + return file_changes_proto_rawDescGZIP(), []int{67} } func (x *GetChangeRequest) GetUUID() []byte { @@ -4962,7 +5122,7 @@ type GetChangeByTicketLinkRequest struct { func (x *GetChangeByTicketLinkRequest) Reset() { *x = GetChangeByTicketLinkRequest{} - mi := &file_changes_proto_msgTypes[66] + mi := &file_changes_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4974,7 +5134,7 @@ func (x *GetChangeByTicketLinkRequest) String() string { func (*GetChangeByTicketLinkRequest) ProtoMessage() {} func (x *GetChangeByTicketLinkRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[66] + mi := &file_changes_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4987,7 +5147,7 @@ func (x *GetChangeByTicketLinkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeByTicketLinkRequest.ProtoReflect.Descriptor instead. func (*GetChangeByTicketLinkRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{66} + return file_changes_proto_rawDescGZIP(), []int{68} } func (x *GetChangeByTicketLinkRequest) GetTicketLink() string { @@ -5017,7 +5177,7 @@ type GetChangeSummaryRequest struct { func (x *GetChangeSummaryRequest) Reset() { *x = GetChangeSummaryRequest{} - mi := &file_changes_proto_msgTypes[67] + mi := &file_changes_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5029,7 +5189,7 @@ func (x *GetChangeSummaryRequest) String() string { func (*GetChangeSummaryRequest) ProtoMessage() {} func (x *GetChangeSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[67] + mi := &file_changes_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5042,7 +5202,7 @@ func (x *GetChangeSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSummaryRequest.ProtoReflect.Descriptor instead. func (*GetChangeSummaryRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{67} + return file_changes_proto_rawDescGZIP(), []int{69} } func (x *GetChangeSummaryRequest) GetUUID() []byte { @@ -5089,7 +5249,7 @@ type GetChangeSummaryResponse struct { func (x *GetChangeSummaryResponse) Reset() { *x = GetChangeSummaryResponse{} - mi := &file_changes_proto_msgTypes[68] + mi := &file_changes_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5101,7 +5261,7 @@ func (x *GetChangeSummaryResponse) String() string { func (*GetChangeSummaryResponse) ProtoMessage() {} func (x *GetChangeSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[68] + mi := &file_changes_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5114,7 +5274,7 @@ func (x *GetChangeSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSummaryResponse.ProtoReflect.Descriptor instead. func (*GetChangeSummaryResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{68} + return file_changes_proto_rawDescGZIP(), []int{70} } func (x *GetChangeSummaryResponse) GetChange() string { @@ -5135,7 +5295,7 @@ type GetChangeSignalsRequest struct { func (x *GetChangeSignalsRequest) Reset() { *x = GetChangeSignalsRequest{} - mi := &file_changes_proto_msgTypes[69] + mi := &file_changes_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5147,7 +5307,7 @@ func (x *GetChangeSignalsRequest) String() string { func (*GetChangeSignalsRequest) ProtoMessage() {} func (x *GetChangeSignalsRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[69] + mi := &file_changes_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5160,7 +5320,7 @@ func (x *GetChangeSignalsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSignalsRequest.ProtoReflect.Descriptor instead. func (*GetChangeSignalsRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{69} + return file_changes_proto_rawDescGZIP(), []int{71} } func (x *GetChangeSignalsRequest) GetUUID() []byte { @@ -5186,7 +5346,7 @@ type GetChangeSignalsResponse struct { func (x *GetChangeSignalsResponse) Reset() { *x = GetChangeSignalsResponse{} - mi := &file_changes_proto_msgTypes[70] + mi := &file_changes_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5198,7 +5358,7 @@ func (x *GetChangeSignalsResponse) String() string { func (*GetChangeSignalsResponse) ProtoMessage() {} func (x *GetChangeSignalsResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[70] + mi := &file_changes_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5211,7 +5371,7 @@ func (x *GetChangeSignalsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSignalsResponse.ProtoReflect.Descriptor instead. func (*GetChangeSignalsResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{70} + return file_changes_proto_rawDescGZIP(), []int{72} } func (x *GetChangeSignalsResponse) GetSignals() string { @@ -5230,7 +5390,7 @@ type GetChangeResponse struct { func (x *GetChangeResponse) Reset() { *x = GetChangeResponse{} - mi := &file_changes_proto_msgTypes[71] + mi := &file_changes_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5242,7 +5402,7 @@ func (x *GetChangeResponse) String() string { func (*GetChangeResponse) ProtoMessage() {} func (x *GetChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[71] + mi := &file_changes_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5255,7 +5415,7 @@ func (x *GetChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeResponse.ProtoReflect.Descriptor instead. func (*GetChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{71} + return file_changes_proto_rawDescGZIP(), []int{73} } func (x *GetChangeResponse) GetChange() *Change { @@ -5275,7 +5435,7 @@ type GetChangeRisksRequest struct { func (x *GetChangeRisksRequest) Reset() { *x = GetChangeRisksRequest{} - mi := &file_changes_proto_msgTypes[72] + mi := &file_changes_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5287,7 +5447,7 @@ func (x *GetChangeRisksRequest) String() string { func (*GetChangeRisksRequest) ProtoMessage() {} func (x *GetChangeRisksRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[72] + mi := &file_changes_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5300,7 +5460,7 @@ func (x *GetChangeRisksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRisksRequest.ProtoReflect.Descriptor instead. func (*GetChangeRisksRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{72} + return file_changes_proto_rawDescGZIP(), []int{74} } func (x *GetChangeRisksRequest) GetUUID() []byte { @@ -5328,7 +5488,7 @@ type ChangeRiskMetadata struct { func (x *ChangeRiskMetadata) Reset() { *x = ChangeRiskMetadata{} - mi := &file_changes_proto_msgTypes[73] + mi := &file_changes_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5340,7 +5500,7 @@ func (x *ChangeRiskMetadata) String() string { func (*ChangeRiskMetadata) ProtoMessage() {} func (x *ChangeRiskMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[73] + mi := &file_changes_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5353,7 +5513,7 @@ func (x *ChangeRiskMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeRiskMetadata.ProtoReflect.Descriptor instead. func (*ChangeRiskMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{73} + return file_changes_proto_rawDescGZIP(), []int{75} } func (x *ChangeRiskMetadata) GetChangeAnalysisStatus() *ChangeAnalysisStatus { @@ -5400,7 +5560,7 @@ type GetChangeRisksResponse struct { func (x *GetChangeRisksResponse) Reset() { *x = GetChangeRisksResponse{} - mi := &file_changes_proto_msgTypes[74] + mi := &file_changes_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5412,7 +5572,7 @@ func (x *GetChangeRisksResponse) String() string { func (*GetChangeRisksResponse) ProtoMessage() {} func (x *GetChangeRisksResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[74] + mi := &file_changes_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5425,7 +5585,7 @@ func (x *GetChangeRisksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRisksResponse.ProtoReflect.Descriptor instead. func (*GetChangeRisksResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{74} + return file_changes_proto_rawDescGZIP(), []int{76} } func (x *GetChangeRisksResponse) GetChangeRiskMetadata() *ChangeRiskMetadata { @@ -5446,7 +5606,7 @@ type UpdateChangeRequest struct { func (x *UpdateChangeRequest) Reset() { *x = UpdateChangeRequest{} - mi := &file_changes_proto_msgTypes[75] + mi := &file_changes_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5458,7 +5618,7 @@ func (x *UpdateChangeRequest) String() string { func (*UpdateChangeRequest) ProtoMessage() {} func (x *UpdateChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[75] + mi := &file_changes_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5471,7 +5631,7 @@ func (x *UpdateChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChangeRequest.ProtoReflect.Descriptor instead. func (*UpdateChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{75} + return file_changes_proto_rawDescGZIP(), []int{77} } func (x *UpdateChangeRequest) GetUUID() []byte { @@ -5497,7 +5657,7 @@ type UpdateChangeResponse struct { func (x *UpdateChangeResponse) Reset() { *x = UpdateChangeResponse{} - mi := &file_changes_proto_msgTypes[76] + mi := &file_changes_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5509,7 +5669,7 @@ func (x *UpdateChangeResponse) String() string { func (*UpdateChangeResponse) ProtoMessage() {} func (x *UpdateChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[76] + mi := &file_changes_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5522,7 +5682,7 @@ func (x *UpdateChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChangeResponse.ProtoReflect.Descriptor instead. func (*UpdateChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{76} + return file_changes_proto_rawDescGZIP(), []int{78} } func (x *UpdateChangeResponse) GetChange() *Change { @@ -5542,7 +5702,7 @@ type DeleteChangeRequest struct { func (x *DeleteChangeRequest) Reset() { *x = DeleteChangeRequest{} - mi := &file_changes_proto_msgTypes[77] + mi := &file_changes_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5554,7 +5714,7 @@ func (x *DeleteChangeRequest) String() string { func (*DeleteChangeRequest) ProtoMessage() {} func (x *DeleteChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[77] + mi := &file_changes_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5567,7 +5727,7 @@ func (x *DeleteChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteChangeRequest.ProtoReflect.Descriptor instead. func (*DeleteChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{77} + return file_changes_proto_rawDescGZIP(), []int{79} } func (x *DeleteChangeRequest) GetUUID() []byte { @@ -5587,7 +5747,7 @@ type ListChangesBySnapshotUUIDRequest struct { func (x *ListChangesBySnapshotUUIDRequest) Reset() { *x = ListChangesBySnapshotUUIDRequest{} - mi := &file_changes_proto_msgTypes[78] + mi := &file_changes_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5599,7 +5759,7 @@ func (x *ListChangesBySnapshotUUIDRequest) String() string { func (*ListChangesBySnapshotUUIDRequest) ProtoMessage() {} func (x *ListChangesBySnapshotUUIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[78] + mi := &file_changes_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5612,7 +5772,7 @@ func (x *ListChangesBySnapshotUUIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesBySnapshotUUIDRequest.ProtoReflect.Descriptor instead. func (*ListChangesBySnapshotUUIDRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{78} + return file_changes_proto_rawDescGZIP(), []int{80} } func (x *ListChangesBySnapshotUUIDRequest) GetUUID() []byte { @@ -5631,7 +5791,7 @@ type ListChangesBySnapshotUUIDResponse struct { func (x *ListChangesBySnapshotUUIDResponse) Reset() { *x = ListChangesBySnapshotUUIDResponse{} - mi := &file_changes_proto_msgTypes[79] + mi := &file_changes_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5643,7 +5803,7 @@ func (x *ListChangesBySnapshotUUIDResponse) String() string { func (*ListChangesBySnapshotUUIDResponse) ProtoMessage() {} func (x *ListChangesBySnapshotUUIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[79] + mi := &file_changes_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5656,7 +5816,7 @@ func (x *ListChangesBySnapshotUUIDResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListChangesBySnapshotUUIDResponse.ProtoReflect.Descriptor instead. func (*ListChangesBySnapshotUUIDResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{79} + return file_changes_proto_rawDescGZIP(), []int{81} } func (x *ListChangesBySnapshotUUIDResponse) GetChanges() []*Change { @@ -5674,7 +5834,7 @@ type DeleteChangeResponse struct { func (x *DeleteChangeResponse) Reset() { *x = DeleteChangeResponse{} - mi := &file_changes_proto_msgTypes[80] + mi := &file_changes_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5686,7 +5846,7 @@ func (x *DeleteChangeResponse) String() string { func (*DeleteChangeResponse) ProtoMessage() {} func (x *DeleteChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[80] + mi := &file_changes_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5699,7 +5859,7 @@ func (x *DeleteChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteChangeResponse.ProtoReflect.Descriptor instead. func (*DeleteChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{80} + return file_changes_proto_rawDescGZIP(), []int{82} } type RefreshStateRequest struct { @@ -5710,7 +5870,7 @@ type RefreshStateRequest struct { func (x *RefreshStateRequest) Reset() { *x = RefreshStateRequest{} - mi := &file_changes_proto_msgTypes[81] + mi := &file_changes_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5722,7 +5882,7 @@ func (x *RefreshStateRequest) String() string { func (*RefreshStateRequest) ProtoMessage() {} func (x *RefreshStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[81] + mi := &file_changes_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5735,7 +5895,7 @@ func (x *RefreshStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshStateRequest.ProtoReflect.Descriptor instead. func (*RefreshStateRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{81} + return file_changes_proto_rawDescGZIP(), []int{83} } type RefreshStateResponse struct { @@ -5746,7 +5906,7 @@ type RefreshStateResponse struct { func (x *RefreshStateResponse) Reset() { *x = RefreshStateResponse{} - mi := &file_changes_proto_msgTypes[82] + mi := &file_changes_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5758,7 +5918,7 @@ func (x *RefreshStateResponse) String() string { func (*RefreshStateResponse) ProtoMessage() {} func (x *RefreshStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[82] + mi := &file_changes_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5771,7 +5931,7 @@ func (x *RefreshStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshStateResponse.ProtoReflect.Descriptor instead. func (*RefreshStateResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{82} + return file_changes_proto_rawDescGZIP(), []int{84} } type StartChangeRequest struct { @@ -5783,7 +5943,7 @@ type StartChangeRequest struct { func (x *StartChangeRequest) Reset() { *x = StartChangeRequest{} - mi := &file_changes_proto_msgTypes[83] + mi := &file_changes_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5795,7 +5955,7 @@ func (x *StartChangeRequest) String() string { func (*StartChangeRequest) ProtoMessage() {} func (x *StartChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[83] + mi := &file_changes_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5808,7 +5968,7 @@ func (x *StartChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeRequest.ProtoReflect.Descriptor instead. func (*StartChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{83} + return file_changes_proto_rawDescGZIP(), []int{85} } func (x *StartChangeRequest) GetChangeUUID() []byte { @@ -5829,7 +5989,7 @@ type StartChangeResponse struct { func (x *StartChangeResponse) Reset() { *x = StartChangeResponse{} - mi := &file_changes_proto_msgTypes[84] + mi := &file_changes_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5841,7 +6001,7 @@ func (x *StartChangeResponse) String() string { func (*StartChangeResponse) ProtoMessage() {} func (x *StartChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[84] + mi := &file_changes_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5854,7 +6014,7 @@ func (x *StartChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeResponse.ProtoReflect.Descriptor instead. func (*StartChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{84} + return file_changes_proto_rawDescGZIP(), []int{86} } func (x *StartChangeResponse) GetState() StartChangeResponse_State { @@ -5887,7 +6047,7 @@ type EndChangeRequest struct { func (x *EndChangeRequest) Reset() { *x = EndChangeRequest{} - mi := &file_changes_proto_msgTypes[85] + mi := &file_changes_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5899,7 +6059,7 @@ func (x *EndChangeRequest) String() string { func (*EndChangeRequest) ProtoMessage() {} func (x *EndChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[85] + mi := &file_changes_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5912,7 +6072,7 @@ func (x *EndChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EndChangeRequest.ProtoReflect.Descriptor instead. func (*EndChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{85} + return file_changes_proto_rawDescGZIP(), []int{87} } func (x *EndChangeRequest) GetChangeUUID() []byte { @@ -5933,7 +6093,7 @@ type EndChangeResponse struct { func (x *EndChangeResponse) Reset() { *x = EndChangeResponse{} - mi := &file_changes_proto_msgTypes[86] + mi := &file_changes_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5945,7 +6105,7 @@ func (x *EndChangeResponse) String() string { func (*EndChangeResponse) ProtoMessage() {} func (x *EndChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[86] + mi := &file_changes_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5958,7 +6118,7 @@ func (x *EndChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EndChangeResponse.ProtoReflect.Descriptor instead. func (*EndChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{86} + return file_changes_proto_rawDescGZIP(), []int{88} } func (x *EndChangeResponse) GetState() EndChangeResponse_State { @@ -5990,7 +6150,7 @@ type StartChangeSimpleResponse struct { func (x *StartChangeSimpleResponse) Reset() { *x = StartChangeSimpleResponse{} - mi := &file_changes_proto_msgTypes[87] + mi := &file_changes_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6002,7 +6162,7 @@ func (x *StartChangeSimpleResponse) String() string { func (*StartChangeSimpleResponse) ProtoMessage() {} func (x *StartChangeSimpleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[87] + mi := &file_changes_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6015,7 +6175,7 @@ func (x *StartChangeSimpleResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeSimpleResponse.ProtoReflect.Descriptor instead. func (*StartChangeSimpleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{87} + return file_changes_proto_rawDescGZIP(), []int{89} } type EndChangeSimpleResponse struct { @@ -6030,7 +6190,7 @@ type EndChangeSimpleResponse struct { func (x *EndChangeSimpleResponse) Reset() { *x = EndChangeSimpleResponse{} - mi := &file_changes_proto_msgTypes[88] + mi := &file_changes_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6042,7 +6202,7 @@ func (x *EndChangeSimpleResponse) String() string { func (*EndChangeSimpleResponse) ProtoMessage() {} func (x *EndChangeSimpleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[88] + mi := &file_changes_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6055,7 +6215,7 @@ func (x *EndChangeSimpleResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EndChangeSimpleResponse.ProtoReflect.Descriptor instead. func (*EndChangeSimpleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{88} + return file_changes_proto_rawDescGZIP(), []int{90} } func (x *EndChangeSimpleResponse) GetQueued() bool { @@ -6085,7 +6245,7 @@ type Risk struct { func (x *Risk) Reset() { *x = Risk{} - mi := &file_changes_proto_msgTypes[89] + mi := &file_changes_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6097,7 +6257,7 @@ func (x *Risk) String() string { func (*Risk) ProtoMessage() {} func (x *Risk) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[89] + mi := &file_changes_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6110,7 +6270,7 @@ func (x *Risk) ProtoReflect() protoreflect.Message { // Deprecated: Use Risk.ProtoReflect.Descriptor instead. func (*Risk) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{89} + return file_changes_proto_rawDescGZIP(), []int{91} } func (x *Risk) GetUUID() []byte { @@ -6157,7 +6317,7 @@ type ChangeAnalysisStatus struct { func (x *ChangeAnalysisStatus) Reset() { *x = ChangeAnalysisStatus{} - mi := &file_changes_proto_msgTypes[90] + mi := &file_changes_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6169,7 +6329,7 @@ func (x *ChangeAnalysisStatus) String() string { func (*ChangeAnalysisStatus) ProtoMessage() {} func (x *ChangeAnalysisStatus) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[90] + mi := &file_changes_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6182,7 +6342,7 @@ func (x *ChangeAnalysisStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeAnalysisStatus.ProtoReflect.Descriptor instead. func (*ChangeAnalysisStatus) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{90} + return file_changes_proto_rawDescGZIP(), []int{92} } func (x *ChangeAnalysisStatus) GetStatus() ChangeAnalysisStatus_Status { @@ -6203,7 +6363,7 @@ type GenerateRiskFixRequest struct { func (x *GenerateRiskFixRequest) Reset() { *x = GenerateRiskFixRequest{} - mi := &file_changes_proto_msgTypes[91] + mi := &file_changes_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6215,7 +6375,7 @@ func (x *GenerateRiskFixRequest) String() string { func (*GenerateRiskFixRequest) ProtoMessage() {} func (x *GenerateRiskFixRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[91] + mi := &file_changes_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6228,7 +6388,7 @@ func (x *GenerateRiskFixRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateRiskFixRequest.ProtoReflect.Descriptor instead. func (*GenerateRiskFixRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{91} + return file_changes_proto_rawDescGZIP(), []int{93} } func (x *GenerateRiskFixRequest) GetRiskUUID() []byte { @@ -6248,7 +6408,7 @@ type GenerateRiskFixResponse struct { func (x *GenerateRiskFixResponse) Reset() { *x = GenerateRiskFixResponse{} - mi := &file_changes_proto_msgTypes[92] + mi := &file_changes_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6260,7 +6420,7 @@ func (x *GenerateRiskFixResponse) String() string { func (*GenerateRiskFixResponse) ProtoMessage() {} func (x *GenerateRiskFixResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[92] + mi := &file_changes_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6273,7 +6433,7 @@ func (x *GenerateRiskFixResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateRiskFixResponse.ProtoReflect.Descriptor instead. func (*GenerateRiskFixResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{92} + return file_changes_proto_rawDescGZIP(), []int{94} } func (x *GenerateRiskFixResponse) GetFixSuggestion() string { @@ -6303,7 +6463,7 @@ type ChangeMetadata_HealthChange struct { func (x *ChangeMetadata_HealthChange) Reset() { *x = ChangeMetadata_HealthChange{} - mi := &file_changes_proto_msgTypes[95] + mi := &file_changes_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6315,7 +6475,7 @@ func (x *ChangeMetadata_HealthChange) String() string { func (*ChangeMetadata_HealthChange) ProtoMessage() {} func (x *ChangeMetadata_HealthChange) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[95] + mi := &file_changes_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6328,7 +6488,7 @@ func (x *ChangeMetadata_HealthChange) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeMetadata_HealthChange.ProtoReflect.Descriptor instead. func (*ChangeMetadata_HealthChange) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{56, 0} + return file_changes_proto_rawDescGZIP(), []int{58, 0} } func (x *ChangeMetadata_HealthChange) GetAdded() int32 { @@ -6414,7 +6574,15 @@ const file_changes_proto_rawDesc = "" + "#ReapplyLabelRuleInTimeRangeResponse\x12\x1e\n" + "\n" + "changeUUID\x18\x01 \x03(\fR\n" + - "changeUUID\"=\n" + + "changeUUID\"D\n" + + "\x12KnowledgeReference\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" + + "\bfileName\x18\x02 \x01(\tR\bfileName\"w\n" + + "\tKnowledge\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\x12\x1a\n" + + "\bfileName\x18\x04 \x01(\tR\bfileName\"=\n" + "\x1bGetHypothesesDetailsRequest\x12\x1e\n" + "\n" + "changeUUID\x18\x01 \x01(\fR\n" + @@ -6422,13 +6590,14 @@ const file_changes_proto_rawDesc = "" + "\x1cGetHypothesesDetailsResponse\x12:\n" + "\n" + "hypotheses\x18\x01 \x03(\v2\x1a.changes.HypothesesDetailsR\n" + - "hypotheses\"\xd2\x01\n" + + "hypotheses\"\x95\x02\n" + "\x11HypothesesDetails\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12(\n" + "\x0fnumObservations\x18\x02 \x01(\rR\x0fnumObservations\x12\x16\n" + "\x06detail\x18\x03 \x01(\tR\x06detail\x121\n" + "\x06status\x18\x04 \x01(\x0e2\x19.changes.HypothesisStatusR\x06status\x122\n" + - "\x14investigationResults\x18\x05 \x01(\tR\x14investigationResults\"<\n" + + "\x14investigationResults\x18\x05 \x01(\tR\x14investigationResults\x12A\n" + + "\rknowledgeUsed\x18\x06 \x03(\v2\x1b.changes.KnowledgeReferenceR\rknowledgeUsed\"<\n" + "\x1aGetChangeTimelineV2Request\x12\x1e\n" + "\n" + "changeUUID\x18\x01 \x01(\fR\n" + @@ -6476,14 +6645,17 @@ const file_changes_proto_rawDesc = "" + "\rnumHypotheses\x18\x01 \x01(\rR\rnumHypotheses\x12:\n" + "\n" + "hypotheses\x18\x02 \x03(\v2\x1a.changes.HypothesisSummaryR\n" + - "hypotheses\"\xce\x01\n" + + "hypotheses\"\xee\x01\n" + "\"InvestigateHypothesesTimelineEntry\x12\x1c\n" + "\tnumProven\x18\x01 \x01(\rR\tnumProven\x12\"\n" + "\fnumDisproven\x18\x02 \x01(\rR\fnumDisproven\x12*\n" + "\x10numInvestigating\x18\x03 \x01(\rR\x10numInvestigating\x12:\n" + "\n" + "hypotheses\x18\x04 \x03(\v2\x1a.changes.HypothesisSummaryR\n" + - "hypotheses\"t\n" + + "hypotheses\x12\x1e\n" + + "\n" + + "numSkipped\x18\x05 \x01(\rR\n" + + "numSkipped\"t\n" + "\x11HypothesisSummary\x121\n" + "\x06status\x18\x01 \x01(\x0e2\x19.changes.HypothesisStatusR\x06status\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" + @@ -6520,7 +6692,7 @@ const file_changes_proto_rawDesc = "" + "\x0emapping_status\x18\x04 \x01(\x0e2 .changes.MappedItemMappingStatusH\x02R\rmappingStatus\x88\x01\x01B\x0f\n" + "\r_mappingQueryB\x0f\n" + "\r_mappingErrorB\x11\n" + - "\x0f_mapping_status\"\xa1\x04\n" + + "\x0f_mapping_status\"\xd3\x04\n" + "\x1aStartChangeAnalysisRequest\x12\x1e\n" + "\n" + "changeUUID\x18\x01 \x01(\fR\n" + @@ -6528,7 +6700,8 @@ const file_changes_proto_rawDesc = "" + "\rchangingItems\x18\x02 \x03(\v2\x17.changes.MappedItemDiffR\rchangingItems\x12\\\n" + "\x19blastRadiusConfigOverride\x18\x03 \x01(\v2\x19.config.BlastRadiusConfigH\x00R\x19blastRadiusConfigOverride\x88\x01\x01\x12e\n" + "\x1croutineChangesConfigOverride\x18\x05 \x01(\v2\x1c.config.RoutineChangesConfigH\x01R\x1croutineChangesConfigOverride\x88\x01\x01\x12t\n" + - "!githubOrganisationProfileOverride\x18\x06 \x01(\v2!.config.GithubOrganisationProfileH\x02R!githubOrganisationProfileOverride\x88\x01\x01B\x1c\n" + + "!githubOrganisationProfileOverride\x18\x06 \x01(\v2!.config.GithubOrganisationProfileH\x02R!githubOrganisationProfileOverride\x88\x01\x01\x120\n" + + "\tknowledge\x18\a \x03(\v2\x12.changes.KnowledgeR\tknowledgeB\x1c\n" + "\x1a_blastRadiusConfigOverrideB\x1f\n" + "\x1d_routineChangesConfigOverrideB$\n" + "\"_githubOrganisationProfileOverrideJ\x04\b\x04\x10\x05\"\x1d\n" + @@ -6624,7 +6797,8 @@ const file_changes_proto_rawDesc = "" + "\bmetadata\x18\x01 \x01(\v2\x17.changes.ChangeMetadataR\bmetadata\x129\n" + "\n" + "properties\x18\x02 \x01(\v2\x19.changes.ChangePropertiesR\n" + - "properties\"\xdf\t\n" + + "properties\"\xb2\n" + + "\n" + "\x0eChangeMetadata\x12\x12\n" + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x128\n" + "\tcreatedAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x128\n" + @@ -6646,7 +6820,8 @@ const file_changes_proto_rawDesc = "" + "\x11ErrorHealthChange\x18\x0f \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x11ErrorHealthChange\x12V\n" + "\x13PendingHealthChange\x18\x10 \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x13PendingHealthChange\x12E\n" + "\x10githubChangeInfo\x18\x14 \x01(\v2\x19.changes.GithubChangeInfoR\x10githubChangeInfo\x122\n" + - "\x12total_observations\x18\x15 \x01(\rH\x00R\x11totalObservations\x88\x01\x01\x1a^\n" + + "\x12total_observations\x18\x15 \x01(\rH\x00R\x11totalObservations\x88\x01\x01\x12Q\n" + + "\x14changeAnalysisStatus\x18\x16 \x01(\v2\x1d.changes.ChangeAnalysisStatusR\x14changeAnalysisStatus\x1a^\n" + "\fHealthChange\x12\x14\n" + "\x05added\x18\x01 \x01(\x05R\x05added\x12\x18\n" + "\aremoved\x18\x02 \x01(\x05R\aremoved\x12\x1e\n" + @@ -6812,13 +6987,14 @@ const file_changes_proto_rawDesc = "" + "\"MAPPED_ITEM_MAPPING_STATUS_SUCCESS\x10\x01\x12*\n" + "&MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED\x10\x02\x12/\n" + "+MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION\x10\x03\x12$\n" + - " MAPPED_ITEM_MAPPING_STATUS_ERROR\x10\x04*\xf9\x01\n" + + " MAPPED_ITEM_MAPPING_STATUS_ERROR\x10\x04*\xa5\x02\n" + "\x10HypothesisStatus\x12.\n" + "*INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED\x10\x00\x12*\n" + "&INVESTIGATED_HYPOTHESIS_STATUS_FORMING\x10\x01\x120\n" + ",INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING\x10\x02\x12)\n" + "%INVESTIGATED_HYPOTHESIS_STATUS_PROVEN\x10\x03\x12,\n" + - "(INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN\x10\x04*_\n" + + "(INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN\x10\x04\x12*\n" + + "&INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED\x10\x05*_\n" + "\x19ChangeTimelineEntryStatus\x12\x0f\n" + "\vUNSPECIFIED\x10\x00\x12\v\n" + "\aPENDING\x10\x01\x12\x0f\n" + @@ -6878,7 +7054,7 @@ const file_changes_proto_rawDesc = "" + "\x0fUpdateLabelRule\x12\x1f.changes.UpdateLabelRuleRequest\x1a .changes.UpdateLabelRuleResponse\x12T\n" + "\x0fDeleteLabelRule\x12\x1f.changes.DeleteLabelRuleRequest\x1a .changes.DeleteLabelRuleResponse\x12P\n" + "\rTestLabelRule\x12\x1d.changes.TestLabelRuleRequest\x1a\x1e.changes.TestLabelRuleResponse0\x01\x12x\n" + - "\x1bReapplyLabelRuleInTimeRange\x12+.changes.ReapplyLabelRuleInTimeRangeRequest\x1a,.changes.ReapplyLabelRuleInTimeRangeResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x1bReapplyLabelRuleInTimeRange\x12+.changes.ReapplyLabelRuleInTimeRangeRequest\x1a,.changes.ReapplyLabelRuleInTimeRangeResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_changes_proto_rawDescOnce sync.Once @@ -6893,7 +7069,7 @@ func file_changes_proto_rawDescGZIP() []byte { } var file_changes_proto_enumTypes = make([]protoimpl.EnumInfo, 12) -var file_changes_proto_msgTypes = make([]protoimpl.MessageInfo, 97) +var file_changes_proto_msgTypes = make([]protoimpl.MessageInfo, 99) var file_changes_proto_goTypes = []any{ (MappedItemTimelineStatus)(0), // 0: changes.MappedItemTimelineStatus (MappedItemMappingStatus)(0), // 1: changes.MappedItemMappingStatus @@ -6924,105 +7100,107 @@ var file_changes_proto_goTypes = []any{ (*TestLabelRuleResponse)(nil), // 26: changes.TestLabelRuleResponse (*ReapplyLabelRuleInTimeRangeRequest)(nil), // 27: changes.ReapplyLabelRuleInTimeRangeRequest (*ReapplyLabelRuleInTimeRangeResponse)(nil), // 28: changes.ReapplyLabelRuleInTimeRangeResponse - (*GetHypothesesDetailsRequest)(nil), // 29: changes.GetHypothesesDetailsRequest - (*GetHypothesesDetailsResponse)(nil), // 30: changes.GetHypothesesDetailsResponse - (*HypothesesDetails)(nil), // 31: changes.HypothesesDetails - (*GetChangeTimelineV2Request)(nil), // 32: changes.GetChangeTimelineV2Request - (*GetChangeTimelineV2Response)(nil), // 33: changes.GetChangeTimelineV2Response - (*ChangeTimelineEntryV2)(nil), // 34: changes.ChangeTimelineEntryV2 - (*EmptyContent)(nil), // 35: changes.EmptyContent - (*MappedItemTimelineSummary)(nil), // 36: changes.MappedItemTimelineSummary - (*MappedItemsTimelineEntry)(nil), // 37: changes.MappedItemsTimelineEntry - (*CalculatedBlastRadiusTimelineEntry)(nil), // 38: changes.CalculatedBlastRadiusTimelineEntry - (*RecordObservationsTimelineEntry)(nil), // 39: changes.RecordObservationsTimelineEntry - (*FormHypothesesTimelineEntry)(nil), // 40: changes.FormHypothesesTimelineEntry - (*InvestigateHypothesesTimelineEntry)(nil), // 41: changes.InvestigateHypothesesTimelineEntry - (*HypothesisSummary)(nil), // 42: changes.HypothesisSummary - (*CalculatedRisksTimelineEntry)(nil), // 43: changes.CalculatedRisksTimelineEntry - (*CalculatedLabelsTimelineEntry)(nil), // 44: changes.CalculatedLabelsTimelineEntry - (*ChangeValidationTimelineEntry)(nil), // 45: changes.ChangeValidationTimelineEntry - (*ChangeValidationCategory)(nil), // 46: changes.ChangeValidationCategory - (*GetDiffRequest)(nil), // 47: changes.GetDiffRequest - (*GetDiffResponse)(nil), // 48: changes.GetDiffResponse - (*ListChangingItemsSummaryRequest)(nil), // 49: changes.ListChangingItemsSummaryRequest - (*ListChangingItemsSummaryResponse)(nil), // 50: changes.ListChangingItemsSummaryResponse - (*MappedItemDiff)(nil), // 51: changes.MappedItemDiff - (*StartChangeAnalysisRequest)(nil), // 52: changes.StartChangeAnalysisRequest - (*StartChangeAnalysisResponse)(nil), // 53: changes.StartChangeAnalysisResponse - (*ListHomeChangesRequest)(nil), // 54: changes.ListHomeChangesRequest - (*ChangeFiltersRequest)(nil), // 55: changes.ChangeFiltersRequest - (*ListHomeChangesResponse)(nil), // 56: changes.ListHomeChangesResponse - (*PopulateChangeFiltersRequest)(nil), // 57: changes.PopulateChangeFiltersRequest - (*PopulateChangeFiltersResponse)(nil), // 58: changes.PopulateChangeFiltersResponse - (*ItemDiffSummary)(nil), // 59: changes.ItemDiffSummary - (*ItemDiff)(nil), // 60: changes.ItemDiff - (*EnrichedTags)(nil), // 61: changes.EnrichedTags - (*TagValue)(nil), // 62: changes.TagValue - (*UserTagValue)(nil), // 63: changes.UserTagValue - (*AutoTagValue)(nil), // 64: changes.AutoTagValue - (*Label)(nil), // 65: changes.Label - (*ChangeSummary)(nil), // 66: changes.ChangeSummary - (*Change)(nil), // 67: changes.Change - (*ChangeMetadata)(nil), // 68: changes.ChangeMetadata - (*ChangeProperties)(nil), // 69: changes.ChangeProperties - (*GithubChangeInfo)(nil), // 70: changes.GithubChangeInfo - (*ListChangesRequest)(nil), // 71: changes.ListChangesRequest - (*ListChangesResponse)(nil), // 72: changes.ListChangesResponse - (*ListChangesByStatusRequest)(nil), // 73: changes.ListChangesByStatusRequest - (*ListChangesByStatusResponse)(nil), // 74: changes.ListChangesByStatusResponse - (*CreateChangeRequest)(nil), // 75: changes.CreateChangeRequest - (*CreateChangeResponse)(nil), // 76: changes.CreateChangeResponse - (*GetChangeRequest)(nil), // 77: changes.GetChangeRequest - (*GetChangeByTicketLinkRequest)(nil), // 78: changes.GetChangeByTicketLinkRequest - (*GetChangeSummaryRequest)(nil), // 79: changes.GetChangeSummaryRequest - (*GetChangeSummaryResponse)(nil), // 80: changes.GetChangeSummaryResponse - (*GetChangeSignalsRequest)(nil), // 81: changes.GetChangeSignalsRequest - (*GetChangeSignalsResponse)(nil), // 82: changes.GetChangeSignalsResponse - (*GetChangeResponse)(nil), // 83: changes.GetChangeResponse - (*GetChangeRisksRequest)(nil), // 84: changes.GetChangeRisksRequest - (*ChangeRiskMetadata)(nil), // 85: changes.ChangeRiskMetadata - (*GetChangeRisksResponse)(nil), // 86: changes.GetChangeRisksResponse - (*UpdateChangeRequest)(nil), // 87: changes.UpdateChangeRequest - (*UpdateChangeResponse)(nil), // 88: changes.UpdateChangeResponse - (*DeleteChangeRequest)(nil), // 89: changes.DeleteChangeRequest - (*ListChangesBySnapshotUUIDRequest)(nil), // 90: changes.ListChangesBySnapshotUUIDRequest - (*ListChangesBySnapshotUUIDResponse)(nil), // 91: changes.ListChangesBySnapshotUUIDResponse - (*DeleteChangeResponse)(nil), // 92: changes.DeleteChangeResponse - (*RefreshStateRequest)(nil), // 93: changes.RefreshStateRequest - (*RefreshStateResponse)(nil), // 94: changes.RefreshStateResponse - (*StartChangeRequest)(nil), // 95: changes.StartChangeRequest - (*StartChangeResponse)(nil), // 96: changes.StartChangeResponse - (*EndChangeRequest)(nil), // 97: changes.EndChangeRequest - (*EndChangeResponse)(nil), // 98: changes.EndChangeResponse - (*StartChangeSimpleResponse)(nil), // 99: changes.StartChangeSimpleResponse - (*EndChangeSimpleResponse)(nil), // 100: changes.EndChangeSimpleResponse - (*Risk)(nil), // 101: changes.Risk - (*ChangeAnalysisStatus)(nil), // 102: changes.ChangeAnalysisStatus - (*GenerateRiskFixRequest)(nil), // 103: changes.GenerateRiskFixRequest - (*GenerateRiskFixResponse)(nil), // 104: changes.GenerateRiskFixResponse - nil, // 105: changes.EnrichedTags.TagValueEntry - nil, // 106: changes.ChangeSummary.TagsEntry - (*ChangeMetadata_HealthChange)(nil), // 107: changes.ChangeMetadata.HealthChange - nil, // 108: changes.ChangeProperties.TagsEntry - (*timestamppb.Timestamp)(nil), // 109: google.protobuf.Timestamp - (*Edge)(nil), // 110: Edge - (*Query)(nil), // 111: Query - (*QueryError)(nil), // 112: QueryError - (*BlastRadiusConfig)(nil), // 113: config.BlastRadiusConfig - (*RoutineChangesConfig)(nil), // 114: config.RoutineChangesConfig - (*GithubOrganisationProfile)(nil), // 115: config.GithubOrganisationProfile - (*PaginationRequest)(nil), // 116: PaginationRequest - (SortOrder)(0), // 117: SortOrder - (*PaginationResponse)(nil), // 118: PaginationResponse - (*Reference)(nil), // 119: Reference - (Health)(0), // 120: Health - (*Item)(nil), // 121: Item + (*KnowledgeReference)(nil), // 29: changes.KnowledgeReference + (*Knowledge)(nil), // 30: changes.Knowledge + (*GetHypothesesDetailsRequest)(nil), // 31: changes.GetHypothesesDetailsRequest + (*GetHypothesesDetailsResponse)(nil), // 32: changes.GetHypothesesDetailsResponse + (*HypothesesDetails)(nil), // 33: changes.HypothesesDetails + (*GetChangeTimelineV2Request)(nil), // 34: changes.GetChangeTimelineV2Request + (*GetChangeTimelineV2Response)(nil), // 35: changes.GetChangeTimelineV2Response + (*ChangeTimelineEntryV2)(nil), // 36: changes.ChangeTimelineEntryV2 + (*EmptyContent)(nil), // 37: changes.EmptyContent + (*MappedItemTimelineSummary)(nil), // 38: changes.MappedItemTimelineSummary + (*MappedItemsTimelineEntry)(nil), // 39: changes.MappedItemsTimelineEntry + (*CalculatedBlastRadiusTimelineEntry)(nil), // 40: changes.CalculatedBlastRadiusTimelineEntry + (*RecordObservationsTimelineEntry)(nil), // 41: changes.RecordObservationsTimelineEntry + (*FormHypothesesTimelineEntry)(nil), // 42: changes.FormHypothesesTimelineEntry + (*InvestigateHypothesesTimelineEntry)(nil), // 43: changes.InvestigateHypothesesTimelineEntry + (*HypothesisSummary)(nil), // 44: changes.HypothesisSummary + (*CalculatedRisksTimelineEntry)(nil), // 45: changes.CalculatedRisksTimelineEntry + (*CalculatedLabelsTimelineEntry)(nil), // 46: changes.CalculatedLabelsTimelineEntry + (*ChangeValidationTimelineEntry)(nil), // 47: changes.ChangeValidationTimelineEntry + (*ChangeValidationCategory)(nil), // 48: changes.ChangeValidationCategory + (*GetDiffRequest)(nil), // 49: changes.GetDiffRequest + (*GetDiffResponse)(nil), // 50: changes.GetDiffResponse + (*ListChangingItemsSummaryRequest)(nil), // 51: changes.ListChangingItemsSummaryRequest + (*ListChangingItemsSummaryResponse)(nil), // 52: changes.ListChangingItemsSummaryResponse + (*MappedItemDiff)(nil), // 53: changes.MappedItemDiff + (*StartChangeAnalysisRequest)(nil), // 54: changes.StartChangeAnalysisRequest + (*StartChangeAnalysisResponse)(nil), // 55: changes.StartChangeAnalysisResponse + (*ListHomeChangesRequest)(nil), // 56: changes.ListHomeChangesRequest + (*ChangeFiltersRequest)(nil), // 57: changes.ChangeFiltersRequest + (*ListHomeChangesResponse)(nil), // 58: changes.ListHomeChangesResponse + (*PopulateChangeFiltersRequest)(nil), // 59: changes.PopulateChangeFiltersRequest + (*PopulateChangeFiltersResponse)(nil), // 60: changes.PopulateChangeFiltersResponse + (*ItemDiffSummary)(nil), // 61: changes.ItemDiffSummary + (*ItemDiff)(nil), // 62: changes.ItemDiff + (*EnrichedTags)(nil), // 63: changes.EnrichedTags + (*TagValue)(nil), // 64: changes.TagValue + (*UserTagValue)(nil), // 65: changes.UserTagValue + (*AutoTagValue)(nil), // 66: changes.AutoTagValue + (*Label)(nil), // 67: changes.Label + (*ChangeSummary)(nil), // 68: changes.ChangeSummary + (*Change)(nil), // 69: changes.Change + (*ChangeMetadata)(nil), // 70: changes.ChangeMetadata + (*ChangeProperties)(nil), // 71: changes.ChangeProperties + (*GithubChangeInfo)(nil), // 72: changes.GithubChangeInfo + (*ListChangesRequest)(nil), // 73: changes.ListChangesRequest + (*ListChangesResponse)(nil), // 74: changes.ListChangesResponse + (*ListChangesByStatusRequest)(nil), // 75: changes.ListChangesByStatusRequest + (*ListChangesByStatusResponse)(nil), // 76: changes.ListChangesByStatusResponse + (*CreateChangeRequest)(nil), // 77: changes.CreateChangeRequest + (*CreateChangeResponse)(nil), // 78: changes.CreateChangeResponse + (*GetChangeRequest)(nil), // 79: changes.GetChangeRequest + (*GetChangeByTicketLinkRequest)(nil), // 80: changes.GetChangeByTicketLinkRequest + (*GetChangeSummaryRequest)(nil), // 81: changes.GetChangeSummaryRequest + (*GetChangeSummaryResponse)(nil), // 82: changes.GetChangeSummaryResponse + (*GetChangeSignalsRequest)(nil), // 83: changes.GetChangeSignalsRequest + (*GetChangeSignalsResponse)(nil), // 84: changes.GetChangeSignalsResponse + (*GetChangeResponse)(nil), // 85: changes.GetChangeResponse + (*GetChangeRisksRequest)(nil), // 86: changes.GetChangeRisksRequest + (*ChangeRiskMetadata)(nil), // 87: changes.ChangeRiskMetadata + (*GetChangeRisksResponse)(nil), // 88: changes.GetChangeRisksResponse + (*UpdateChangeRequest)(nil), // 89: changes.UpdateChangeRequest + (*UpdateChangeResponse)(nil), // 90: changes.UpdateChangeResponse + (*DeleteChangeRequest)(nil), // 91: changes.DeleteChangeRequest + (*ListChangesBySnapshotUUIDRequest)(nil), // 92: changes.ListChangesBySnapshotUUIDRequest + (*ListChangesBySnapshotUUIDResponse)(nil), // 93: changes.ListChangesBySnapshotUUIDResponse + (*DeleteChangeResponse)(nil), // 94: changes.DeleteChangeResponse + (*RefreshStateRequest)(nil), // 95: changes.RefreshStateRequest + (*RefreshStateResponse)(nil), // 96: changes.RefreshStateResponse + (*StartChangeRequest)(nil), // 97: changes.StartChangeRequest + (*StartChangeResponse)(nil), // 98: changes.StartChangeResponse + (*EndChangeRequest)(nil), // 99: changes.EndChangeRequest + (*EndChangeResponse)(nil), // 100: changes.EndChangeResponse + (*StartChangeSimpleResponse)(nil), // 101: changes.StartChangeSimpleResponse + (*EndChangeSimpleResponse)(nil), // 102: changes.EndChangeSimpleResponse + (*Risk)(nil), // 103: changes.Risk + (*ChangeAnalysisStatus)(nil), // 104: changes.ChangeAnalysisStatus + (*GenerateRiskFixRequest)(nil), // 105: changes.GenerateRiskFixRequest + (*GenerateRiskFixResponse)(nil), // 106: changes.GenerateRiskFixResponse + nil, // 107: changes.EnrichedTags.TagValueEntry + nil, // 108: changes.ChangeSummary.TagsEntry + (*ChangeMetadata_HealthChange)(nil), // 109: changes.ChangeMetadata.HealthChange + nil, // 110: changes.ChangeProperties.TagsEntry + (*timestamppb.Timestamp)(nil), // 111: google.protobuf.Timestamp + (*Edge)(nil), // 112: Edge + (*Query)(nil), // 113: Query + (*QueryError)(nil), // 114: QueryError + (*BlastRadiusConfig)(nil), // 115: config.BlastRadiusConfig + (*RoutineChangesConfig)(nil), // 116: config.RoutineChangesConfig + (*GithubOrganisationProfile)(nil), // 117: config.GithubOrganisationProfile + (*PaginationRequest)(nil), // 118: PaginationRequest + (SortOrder)(0), // 119: SortOrder + (*PaginationResponse)(nil), // 120: PaginationResponse + (*Reference)(nil), // 121: Reference + (Health)(0), // 122: Health + (*Item)(nil), // 123: Item } var file_changes_proto_depIdxs = []int32{ 13, // 0: changes.LabelRule.metadata:type_name -> changes.LabelRuleMetadata 14, // 1: changes.LabelRule.properties:type_name -> changes.LabelRuleProperties - 109, // 2: changes.LabelRuleMetadata.createdAt:type_name -> google.protobuf.Timestamp - 109, // 3: changes.LabelRuleMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 111, // 2: changes.LabelRuleMetadata.createdAt:type_name -> google.protobuf.Timestamp + 111, // 3: changes.LabelRuleMetadata.updatedAt:type_name -> google.protobuf.Timestamp 12, // 4: changes.ListLabelRulesResponse.rules:type_name -> changes.LabelRule 14, // 5: changes.CreateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties 12, // 6: changes.CreateLabelRuleResponse.rule:type_name -> changes.LabelRule @@ -7030,173 +7208,176 @@ var file_changes_proto_depIdxs = []int32{ 14, // 8: changes.UpdateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties 12, // 9: changes.UpdateLabelRuleResponse.rule:type_name -> changes.LabelRule 14, // 10: changes.TestLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties - 65, // 11: changes.TestLabelRuleResponse.label:type_name -> changes.Label - 109, // 12: changes.ReapplyLabelRuleInTimeRangeRequest.startAt:type_name -> google.protobuf.Timestamp - 109, // 13: changes.ReapplyLabelRuleInTimeRangeRequest.endAt:type_name -> google.protobuf.Timestamp - 31, // 14: changes.GetHypothesesDetailsResponse.hypotheses:type_name -> changes.HypothesesDetails + 67, // 11: changes.TestLabelRuleResponse.label:type_name -> changes.Label + 111, // 12: changes.ReapplyLabelRuleInTimeRangeRequest.startAt:type_name -> google.protobuf.Timestamp + 111, // 13: changes.ReapplyLabelRuleInTimeRangeRequest.endAt:type_name -> google.protobuf.Timestamp + 33, // 14: changes.GetHypothesesDetailsResponse.hypotheses:type_name -> changes.HypothesesDetails 2, // 15: changes.HypothesesDetails.status:type_name -> changes.HypothesisStatus - 34, // 16: changes.GetChangeTimelineV2Response.entries:type_name -> changes.ChangeTimelineEntryV2 - 3, // 17: changes.ChangeTimelineEntryV2.status:type_name -> changes.ChangeTimelineEntryStatus - 109, // 18: changes.ChangeTimelineEntryV2.startedAt:type_name -> google.protobuf.Timestamp - 109, // 19: changes.ChangeTimelineEntryV2.endedAt:type_name -> google.protobuf.Timestamp - 37, // 20: changes.ChangeTimelineEntryV2.mappedItems:type_name -> changes.MappedItemsTimelineEntry - 38, // 21: changes.ChangeTimelineEntryV2.calculatedBlastRadius:type_name -> changes.CalculatedBlastRadiusTimelineEntry - 43, // 22: changes.ChangeTimelineEntryV2.calculatedRisks:type_name -> changes.CalculatedRisksTimelineEntry - 35, // 23: changes.ChangeTimelineEntryV2.empty:type_name -> changes.EmptyContent - 45, // 24: changes.ChangeTimelineEntryV2.changeValidation:type_name -> changes.ChangeValidationTimelineEntry - 44, // 25: changes.ChangeTimelineEntryV2.calculatedLabels:type_name -> changes.CalculatedLabelsTimelineEntry - 40, // 26: changes.ChangeTimelineEntryV2.formHypotheses:type_name -> changes.FormHypothesesTimelineEntry - 41, // 27: changes.ChangeTimelineEntryV2.investigateHypotheses:type_name -> changes.InvestigateHypothesesTimelineEntry - 39, // 28: changes.ChangeTimelineEntryV2.recordObservations:type_name -> changes.RecordObservationsTimelineEntry - 0, // 29: changes.MappedItemTimelineSummary.status:type_name -> changes.MappedItemTimelineStatus - 51, // 30: changes.MappedItemsTimelineEntry.mappedItems:type_name -> changes.MappedItemDiff - 36, // 31: changes.MappedItemsTimelineEntry.items:type_name -> changes.MappedItemTimelineSummary - 42, // 32: changes.FormHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary - 42, // 33: changes.InvestigateHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary - 2, // 34: changes.HypothesisSummary.status:type_name -> changes.HypothesisStatus - 101, // 35: changes.CalculatedRisksTimelineEntry.risks:type_name -> changes.Risk - 65, // 36: changes.CalculatedLabelsTimelineEntry.labels:type_name -> changes.Label - 46, // 37: changes.ChangeValidationTimelineEntry.validationChecklist:type_name -> changes.ChangeValidationCategory - 60, // 38: changes.GetDiffResponse.expectedItems:type_name -> changes.ItemDiff - 60, // 39: changes.GetDiffResponse.unexpectedItems:type_name -> changes.ItemDiff - 110, // 40: changes.GetDiffResponse.edges:type_name -> Edge - 60, // 41: changes.GetDiffResponse.missingItems:type_name -> changes.ItemDiff - 59, // 42: changes.ListChangingItemsSummaryResponse.items:type_name -> changes.ItemDiffSummary - 60, // 43: changes.MappedItemDiff.item:type_name -> changes.ItemDiff - 111, // 44: changes.MappedItemDiff.mappingQuery:type_name -> Query - 112, // 45: changes.MappedItemDiff.mappingError:type_name -> QueryError - 1, // 46: changes.MappedItemDiff.mapping_status:type_name -> changes.MappedItemMappingStatus - 51, // 47: changes.StartChangeAnalysisRequest.changingItems:type_name -> changes.MappedItemDiff - 113, // 48: changes.StartChangeAnalysisRequest.blastRadiusConfigOverride:type_name -> config.BlastRadiusConfig - 114, // 49: changes.StartChangeAnalysisRequest.routineChangesConfigOverride:type_name -> config.RoutineChangesConfig - 115, // 50: changes.StartChangeAnalysisRequest.githubOrganisationProfileOverride:type_name -> config.GithubOrganisationProfile - 116, // 51: changes.ListHomeChangesRequest.pagination:type_name -> PaginationRequest - 55, // 52: changes.ListHomeChangesRequest.filters:type_name -> changes.ChangeFiltersRequest - 10, // 53: changes.ChangeFiltersRequest.risks:type_name -> changes.Risk.Severity - 7, // 54: changes.ChangeFiltersRequest.statuses:type_name -> changes.ChangeStatus - 117, // 55: changes.ChangeFiltersRequest.sortOrder:type_name -> SortOrder - 66, // 56: changes.ListHomeChangesResponse.changes:type_name -> changes.ChangeSummary - 118, // 57: changes.ListHomeChangesResponse.pagination:type_name -> PaginationResponse - 119, // 58: changes.ItemDiffSummary.item:type_name -> Reference - 4, // 59: changes.ItemDiffSummary.status:type_name -> changes.ItemDiffStatus - 120, // 60: changes.ItemDiffSummary.healthAfter:type_name -> Health - 119, // 61: changes.ItemDiff.item:type_name -> Reference - 4, // 62: changes.ItemDiff.status:type_name -> changes.ItemDiffStatus - 121, // 63: changes.ItemDiff.before:type_name -> Item - 121, // 64: changes.ItemDiff.after:type_name -> Item - 105, // 65: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry - 63, // 66: changes.TagValue.userTagValue:type_name -> changes.UserTagValue - 64, // 67: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue - 6, // 68: changes.Label.type:type_name -> changes.LabelType - 7, // 69: changes.ChangeSummary.status:type_name -> changes.ChangeStatus - 109, // 70: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp - 106, // 71: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry - 61, // 72: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags - 65, // 73: changes.ChangeSummary.labels:type_name -> changes.Label - 70, // 74: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo - 68, // 75: changes.Change.metadata:type_name -> changes.ChangeMetadata - 69, // 76: changes.Change.properties:type_name -> changes.ChangeProperties - 109, // 77: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp - 109, // 78: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp - 7, // 79: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus - 107, // 80: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 81: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 82: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 83: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 84: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 70, // 85: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo - 60, // 86: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff - 108, // 87: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry - 61, // 88: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags - 65, // 89: changes.ChangeProperties.labels:type_name -> changes.Label - 67, // 90: changes.ListChangesResponse.changes:type_name -> changes.Change - 7, // 91: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus - 67, // 92: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change - 69, // 93: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties - 67, // 94: changes.CreateChangeResponse.change:type_name -> changes.Change - 5, // 95: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat - 10, // 96: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity - 5, // 97: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat - 67, // 98: changes.GetChangeResponse.change:type_name -> changes.Change - 102, // 99: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus - 101, // 100: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk - 85, // 101: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata - 69, // 102: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties - 67, // 103: changes.UpdateChangeResponse.change:type_name -> changes.Change - 67, // 104: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change - 8, // 105: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State - 9, // 106: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State - 10, // 107: changes.Risk.severity:type_name -> changes.Risk.Severity - 119, // 108: changes.Risk.relatedItems:type_name -> Reference - 11, // 109: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status - 62, // 110: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue - 71, // 111: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest - 73, // 112: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest - 75, // 113: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest - 77, // 114: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest - 78, // 115: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest - 79, // 116: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest - 32, // 117: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request - 84, // 118: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest - 87, // 119: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest - 89, // 120: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest - 90, // 121: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest - 93, // 122: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest - 95, // 123: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest - 97, // 124: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest - 95, // 125: changes.ChangesService.StartChangeSimple:input_type -> changes.StartChangeRequest - 97, // 126: changes.ChangesService.EndChangeSimple:input_type -> changes.EndChangeRequest - 54, // 127: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest - 52, // 128: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest - 49, // 129: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest - 47, // 130: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest - 57, // 131: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest - 103, // 132: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest - 29, // 133: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest - 81, // 134: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest - 15, // 135: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest - 17, // 136: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest - 19, // 137: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest - 21, // 138: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest - 23, // 139: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest - 25, // 140: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest - 27, // 141: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest - 72, // 142: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse - 74, // 143: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse - 76, // 144: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse - 83, // 145: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse - 83, // 146: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse - 80, // 147: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse - 33, // 148: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response - 86, // 149: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse - 88, // 150: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse - 92, // 151: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse - 91, // 152: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse - 94, // 153: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse - 96, // 154: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse - 98, // 155: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse - 99, // 156: changes.ChangesService.StartChangeSimple:output_type -> changes.StartChangeSimpleResponse - 100, // 157: changes.ChangesService.EndChangeSimple:output_type -> changes.EndChangeSimpleResponse - 56, // 158: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse - 53, // 159: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse - 50, // 160: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse - 48, // 161: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse - 58, // 162: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse - 104, // 163: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse - 30, // 164: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse - 82, // 165: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse - 16, // 166: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse - 18, // 167: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse - 20, // 168: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse - 22, // 169: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse - 24, // 170: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse - 26, // 171: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse - 28, // 172: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse - 142, // [142:173] is the sub-list for method output_type - 111, // [111:142] is the sub-list for method input_type - 111, // [111:111] is the sub-list for extension type_name - 111, // [111:111] is the sub-list for extension extendee - 0, // [0:111] is the sub-list for field type_name + 29, // 16: changes.HypothesesDetails.knowledgeUsed:type_name -> changes.KnowledgeReference + 36, // 17: changes.GetChangeTimelineV2Response.entries:type_name -> changes.ChangeTimelineEntryV2 + 3, // 18: changes.ChangeTimelineEntryV2.status:type_name -> changes.ChangeTimelineEntryStatus + 111, // 19: changes.ChangeTimelineEntryV2.startedAt:type_name -> google.protobuf.Timestamp + 111, // 20: changes.ChangeTimelineEntryV2.endedAt:type_name -> google.protobuf.Timestamp + 39, // 21: changes.ChangeTimelineEntryV2.mappedItems:type_name -> changes.MappedItemsTimelineEntry + 40, // 22: changes.ChangeTimelineEntryV2.calculatedBlastRadius:type_name -> changes.CalculatedBlastRadiusTimelineEntry + 45, // 23: changes.ChangeTimelineEntryV2.calculatedRisks:type_name -> changes.CalculatedRisksTimelineEntry + 37, // 24: changes.ChangeTimelineEntryV2.empty:type_name -> changes.EmptyContent + 47, // 25: changes.ChangeTimelineEntryV2.changeValidation:type_name -> changes.ChangeValidationTimelineEntry + 46, // 26: changes.ChangeTimelineEntryV2.calculatedLabels:type_name -> changes.CalculatedLabelsTimelineEntry + 42, // 27: changes.ChangeTimelineEntryV2.formHypotheses:type_name -> changes.FormHypothesesTimelineEntry + 43, // 28: changes.ChangeTimelineEntryV2.investigateHypotheses:type_name -> changes.InvestigateHypothesesTimelineEntry + 41, // 29: changes.ChangeTimelineEntryV2.recordObservations:type_name -> changes.RecordObservationsTimelineEntry + 0, // 30: changes.MappedItemTimelineSummary.status:type_name -> changes.MappedItemTimelineStatus + 53, // 31: changes.MappedItemsTimelineEntry.mappedItems:type_name -> changes.MappedItemDiff + 38, // 32: changes.MappedItemsTimelineEntry.items:type_name -> changes.MappedItemTimelineSummary + 44, // 33: changes.FormHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary + 44, // 34: changes.InvestigateHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary + 2, // 35: changes.HypothesisSummary.status:type_name -> changes.HypothesisStatus + 103, // 36: changes.CalculatedRisksTimelineEntry.risks:type_name -> changes.Risk + 67, // 37: changes.CalculatedLabelsTimelineEntry.labels:type_name -> changes.Label + 48, // 38: changes.ChangeValidationTimelineEntry.validationChecklist:type_name -> changes.ChangeValidationCategory + 62, // 39: changes.GetDiffResponse.expectedItems:type_name -> changes.ItemDiff + 62, // 40: changes.GetDiffResponse.unexpectedItems:type_name -> changes.ItemDiff + 112, // 41: changes.GetDiffResponse.edges:type_name -> Edge + 62, // 42: changes.GetDiffResponse.missingItems:type_name -> changes.ItemDiff + 61, // 43: changes.ListChangingItemsSummaryResponse.items:type_name -> changes.ItemDiffSummary + 62, // 44: changes.MappedItemDiff.item:type_name -> changes.ItemDiff + 113, // 45: changes.MappedItemDiff.mappingQuery:type_name -> Query + 114, // 46: changes.MappedItemDiff.mappingError:type_name -> QueryError + 1, // 47: changes.MappedItemDiff.mapping_status:type_name -> changes.MappedItemMappingStatus + 53, // 48: changes.StartChangeAnalysisRequest.changingItems:type_name -> changes.MappedItemDiff + 115, // 49: changes.StartChangeAnalysisRequest.blastRadiusConfigOverride:type_name -> config.BlastRadiusConfig + 116, // 50: changes.StartChangeAnalysisRequest.routineChangesConfigOverride:type_name -> config.RoutineChangesConfig + 117, // 51: changes.StartChangeAnalysisRequest.githubOrganisationProfileOverride:type_name -> config.GithubOrganisationProfile + 30, // 52: changes.StartChangeAnalysisRequest.knowledge:type_name -> changes.Knowledge + 118, // 53: changes.ListHomeChangesRequest.pagination:type_name -> PaginationRequest + 57, // 54: changes.ListHomeChangesRequest.filters:type_name -> changes.ChangeFiltersRequest + 10, // 55: changes.ChangeFiltersRequest.risks:type_name -> changes.Risk.Severity + 7, // 56: changes.ChangeFiltersRequest.statuses:type_name -> changes.ChangeStatus + 119, // 57: changes.ChangeFiltersRequest.sortOrder:type_name -> SortOrder + 68, // 58: changes.ListHomeChangesResponse.changes:type_name -> changes.ChangeSummary + 120, // 59: changes.ListHomeChangesResponse.pagination:type_name -> PaginationResponse + 121, // 60: changes.ItemDiffSummary.item:type_name -> Reference + 4, // 61: changes.ItemDiffSummary.status:type_name -> changes.ItemDiffStatus + 122, // 62: changes.ItemDiffSummary.healthAfter:type_name -> Health + 121, // 63: changes.ItemDiff.item:type_name -> Reference + 4, // 64: changes.ItemDiff.status:type_name -> changes.ItemDiffStatus + 123, // 65: changes.ItemDiff.before:type_name -> Item + 123, // 66: changes.ItemDiff.after:type_name -> Item + 107, // 67: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry + 65, // 68: changes.TagValue.userTagValue:type_name -> changes.UserTagValue + 66, // 69: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue + 6, // 70: changes.Label.type:type_name -> changes.LabelType + 7, // 71: changes.ChangeSummary.status:type_name -> changes.ChangeStatus + 111, // 72: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp + 108, // 73: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry + 63, // 74: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags + 67, // 75: changes.ChangeSummary.labels:type_name -> changes.Label + 72, // 76: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo + 70, // 77: changes.Change.metadata:type_name -> changes.ChangeMetadata + 71, // 78: changes.Change.properties:type_name -> changes.ChangeProperties + 111, // 79: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp + 111, // 80: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 7, // 81: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus + 109, // 82: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 83: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 84: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 85: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 86: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 72, // 87: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo + 104, // 88: changes.ChangeMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 62, // 89: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff + 110, // 90: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry + 63, // 91: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags + 67, // 92: changes.ChangeProperties.labels:type_name -> changes.Label + 69, // 93: changes.ListChangesResponse.changes:type_name -> changes.Change + 7, // 94: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus + 69, // 95: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change + 71, // 96: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties + 69, // 97: changes.CreateChangeResponse.change:type_name -> changes.Change + 5, // 98: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 10, // 99: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity + 5, // 100: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 69, // 101: changes.GetChangeResponse.change:type_name -> changes.Change + 104, // 102: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 103, // 103: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk + 87, // 104: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata + 71, // 105: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties + 69, // 106: changes.UpdateChangeResponse.change:type_name -> changes.Change + 69, // 107: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change + 8, // 108: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State + 9, // 109: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State + 10, // 110: changes.Risk.severity:type_name -> changes.Risk.Severity + 121, // 111: changes.Risk.relatedItems:type_name -> Reference + 11, // 112: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status + 64, // 113: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue + 73, // 114: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest + 75, // 115: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest + 77, // 116: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest + 79, // 117: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest + 80, // 118: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest + 81, // 119: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest + 34, // 120: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request + 86, // 121: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest + 89, // 122: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest + 91, // 123: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest + 92, // 124: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest + 95, // 125: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest + 97, // 126: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest + 99, // 127: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest + 97, // 128: changes.ChangesService.StartChangeSimple:input_type -> changes.StartChangeRequest + 99, // 129: changes.ChangesService.EndChangeSimple:input_type -> changes.EndChangeRequest + 56, // 130: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest + 54, // 131: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest + 51, // 132: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest + 49, // 133: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest + 59, // 134: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest + 105, // 135: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest + 31, // 136: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest + 83, // 137: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest + 15, // 138: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest + 17, // 139: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest + 19, // 140: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest + 21, // 141: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest + 23, // 142: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest + 25, // 143: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest + 27, // 144: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest + 74, // 145: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse + 76, // 146: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse + 78, // 147: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse + 85, // 148: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse + 85, // 149: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse + 82, // 150: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse + 35, // 151: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response + 88, // 152: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse + 90, // 153: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse + 94, // 154: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse + 93, // 155: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse + 96, // 156: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse + 98, // 157: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse + 100, // 158: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse + 101, // 159: changes.ChangesService.StartChangeSimple:output_type -> changes.StartChangeSimpleResponse + 102, // 160: changes.ChangesService.EndChangeSimple:output_type -> changes.EndChangeSimpleResponse + 58, // 161: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse + 55, // 162: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse + 52, // 163: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse + 50, // 164: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse + 60, // 165: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse + 106, // 166: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse + 32, // 167: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse + 84, // 168: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse + 16, // 169: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse + 18, // 170: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse + 20, // 171: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse + 22, // 172: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse + 24, // 173: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse + 26, // 174: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse + 28, // 175: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse + 145, // [145:176] is the sub-list for method output_type + 114, // [114:145] is the sub-list for method input_type + 114, // [114:114] is the sub-list for extension type_name + 114, // [114:114] is the sub-list for extension extendee + 0, // [0:114] is the sub-list for field type_name } func init() { file_changes_proto_init() } @@ -7207,7 +7388,7 @@ func file_changes_proto_init() { file_config_proto_init() file_items_proto_init() file_util_proto_init() - file_changes_proto_msgTypes[22].OneofWrappers = []any{ + file_changes_proto_msgTypes[24].OneofWrappers = []any{ (*ChangeTimelineEntryV2_MappedItems)(nil), (*ChangeTimelineEntryV2_CalculatedBlastRadius)(nil), (*ChangeTimelineEntryV2_CalculatedRisks)(nil), @@ -7220,24 +7401,24 @@ func file_changes_proto_init() { (*ChangeTimelineEntryV2_InvestigateHypotheses)(nil), (*ChangeTimelineEntryV2_RecordObservations)(nil), } - file_changes_proto_msgTypes[24].OneofWrappers = []any{} - file_changes_proto_msgTypes[39].OneofWrappers = []any{} - file_changes_proto_msgTypes[40].OneofWrappers = []any{} + file_changes_proto_msgTypes[26].OneofWrappers = []any{} + file_changes_proto_msgTypes[41].OneofWrappers = []any{} file_changes_proto_msgTypes[42].OneofWrappers = []any{} - file_changes_proto_msgTypes[43].OneofWrappers = []any{} - file_changes_proto_msgTypes[48].OneofWrappers = []any{} - file_changes_proto_msgTypes[50].OneofWrappers = []any{ + file_changes_proto_msgTypes[44].OneofWrappers = []any{} + file_changes_proto_msgTypes[45].OneofWrappers = []any{} + file_changes_proto_msgTypes[50].OneofWrappers = []any{} + file_changes_proto_msgTypes[52].OneofWrappers = []any{ (*TagValue_UserTagValue)(nil), (*TagValue_AutoTagValue)(nil), } - file_changes_proto_msgTypes[56].OneofWrappers = []any{} + file_changes_proto_msgTypes[58].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_changes_proto_rawDesc), len(file_changes_proto_rawDesc)), NumEnums: 12, - NumMessages: 97, + NumMessages: 99, NumExtensions: 0, NumServices: 2, }, diff --git a/sdp-go/changes_test.go b/go/sdp-go/changes_test.go similarity index 100% rename from sdp-go/changes_test.go rename to go/sdp-go/changes_test.go diff --git a/sdp-go/changetimeline.go b/go/sdp-go/changetimeline.go similarity index 100% rename from sdp-go/changetimeline.go rename to go/sdp-go/changetimeline.go diff --git a/sdp-go/changetimeline_test.go b/go/sdp-go/changetimeline_test.go similarity index 100% rename from sdp-go/changetimeline_test.go rename to go/sdp-go/changetimeline_test.go diff --git a/sdp-go/cli.pb.go b/go/sdp-go/cli.pb.go similarity index 98% rename from sdp-go/cli.pb.go rename to go/sdp-go/cli.pb.go index dc282b3b..e5acc064 100644 --- a/sdp-go/cli.pb.go +++ b/go/sdp-go/cli.pb.go @@ -212,7 +212,7 @@ const file_cli_proto_rawDesc = "" + "\x11SetConfigResponse2\x8b\x01\n" + "\rConfigService\x12<\n" + "\tGetConfig\x12\x15.cli.GetConfigRequest\x1a\x16.cli.GetConfigResponse\"\x00\x12<\n" + - "\tSetConfig\x12\x15.cli.SetConfigRequest\x1a\x16.cli.SetConfigResponse\"\x00B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\tSetConfig\x12\x15.cli.SetConfigRequest\x1a\x16.cli.SetConfigResponse\"\x00B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_cli_proto_rawDescOnce sync.Once diff --git a/sdp-go/compare.go b/go/sdp-go/compare.go similarity index 100% rename from sdp-go/compare.go rename to go/sdp-go/compare.go diff --git a/sdp-go/config.pb.go b/go/sdp-go/config.pb.go similarity index 88% rename from sdp-go/config.pb.go rename to go/sdp-go/config.pb.go index a3f022db..c20030ae 100644 --- a/sdp-go/config.pb.go +++ b/go/sdp-go/config.pb.go @@ -181,7 +181,7 @@ func (x RoutineChangesConfig_DurationUnit) Number() protoreflect.EnumNumber { // Deprecated: Use RoutineChangesConfig_DurationUnit.Descriptor instead. func (RoutineChangesConfig_DurationUnit) EnumDescriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{19, 0} + return file_config_proto_rawDescGZIP(), []int{21, 0} } // The config that is used when calculating the blast radius for a change, this @@ -818,6 +818,110 @@ func (*DeleteHcpConfigResponse) Descriptor() ([]byte, []int) { return file_config_proto_rawDescGZIP(), []int{12} } +type ReplaceHcpApiKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The URL that the user should be redirected to after the OAuth process is + // over. This should be a page in the frontend, probably the HCP Terraform + // Integration page. + FinalFrontendRedirect string `protobuf:"bytes,1,opt,name=finalFrontendRedirect,proto3" json:"finalFrontendRedirect,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReplaceHcpApiKeyRequest) Reset() { + *x = ReplaceHcpApiKeyRequest{} + mi := &file_config_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReplaceHcpApiKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReplaceHcpApiKeyRequest) ProtoMessage() {} + +func (x *ReplaceHcpApiKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReplaceHcpApiKeyRequest.ProtoReflect.Descriptor instead. +func (*ReplaceHcpApiKeyRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{13} +} + +func (x *ReplaceHcpApiKeyRequest) GetFinalFrontendRedirect() string { + if x != nil { + return x.FinalFrontendRedirect + } + return "" +} + +type ReplaceHcpApiKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The configuration of the HCP Run Task (endpoint URL and HMAC secret are + // preserved from the existing config) + Config *HcpConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + // The API Key response for the newly created API key. This API key has been + // created but not yet authorised, the user must still be redirected to the + // authorizeURL to complete the process. + ApiKey *CreateAPIKeyResponse `protobuf:"bytes,2,opt,name=apiKey,proto3" json:"apiKey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReplaceHcpApiKeyResponse) Reset() { + *x = ReplaceHcpApiKeyResponse{} + mi := &file_config_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReplaceHcpApiKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReplaceHcpApiKeyResponse) ProtoMessage() {} + +func (x *ReplaceHcpApiKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReplaceHcpApiKeyResponse.ProtoReflect.Descriptor instead. +func (*ReplaceHcpApiKeyResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{14} +} + +func (x *ReplaceHcpApiKeyResponse) GetConfig() *HcpConfig { + if x != nil { + return x.Config + } + return nil +} + +func (x *ReplaceHcpApiKeyResponse) GetApiKey() *CreateAPIKeyResponse { + if x != nil { + return x.ApiKey + } + return nil +} + // Account Signal config type GetSignalConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -827,7 +931,7 @@ type GetSignalConfigRequest struct { func (x *GetSignalConfigRequest) Reset() { *x = GetSignalConfigRequest{} - mi := &file_config_proto_msgTypes[13] + mi := &file_config_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -839,7 +943,7 @@ func (x *GetSignalConfigRequest) String() string { func (*GetSignalConfigRequest) ProtoMessage() {} func (x *GetSignalConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[13] + mi := &file_config_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -852,7 +956,7 @@ func (x *GetSignalConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSignalConfigRequest.ProtoReflect.Descriptor instead. func (*GetSignalConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{13} + return file_config_proto_rawDescGZIP(), []int{15} } type GetSignalConfigResponse struct { @@ -864,7 +968,7 @@ type GetSignalConfigResponse struct { func (x *GetSignalConfigResponse) Reset() { *x = GetSignalConfigResponse{} - mi := &file_config_proto_msgTypes[14] + mi := &file_config_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -876,7 +980,7 @@ func (x *GetSignalConfigResponse) String() string { func (*GetSignalConfigResponse) ProtoMessage() {} func (x *GetSignalConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[14] + mi := &file_config_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -889,7 +993,7 @@ func (x *GetSignalConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSignalConfigResponse.ProtoReflect.Descriptor instead. func (*GetSignalConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{14} + return file_config_proto_rawDescGZIP(), []int{16} } func (x *GetSignalConfigResponse) GetConfig() *SignalConfig { @@ -909,7 +1013,7 @@ type UpdateSignalConfigRequest struct { func (x *UpdateSignalConfigRequest) Reset() { *x = UpdateSignalConfigRequest{} - mi := &file_config_proto_msgTypes[15] + mi := &file_config_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -921,7 +1025,7 @@ func (x *UpdateSignalConfigRequest) String() string { func (*UpdateSignalConfigRequest) ProtoMessage() {} func (x *UpdateSignalConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[15] + mi := &file_config_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -934,7 +1038,7 @@ func (x *UpdateSignalConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSignalConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateSignalConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{15} + return file_config_proto_rawDescGZIP(), []int{17} } func (x *UpdateSignalConfigRequest) GetConfig() *SignalConfig { @@ -953,7 +1057,7 @@ type UpdateSignalConfigResponse struct { func (x *UpdateSignalConfigResponse) Reset() { *x = UpdateSignalConfigResponse{} - mi := &file_config_proto_msgTypes[16] + mi := &file_config_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -965,7 +1069,7 @@ func (x *UpdateSignalConfigResponse) String() string { func (*UpdateSignalConfigResponse) ProtoMessage() {} func (x *UpdateSignalConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[16] + mi := &file_config_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -978,7 +1082,7 @@ func (x *UpdateSignalConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSignalConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateSignalConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{16} + return file_config_proto_rawDescGZIP(), []int{18} } func (x *UpdateSignalConfigResponse) GetConfig() *SignalConfig { @@ -1002,7 +1106,7 @@ type SignalConfig struct { func (x *SignalConfig) Reset() { *x = SignalConfig{} - mi := &file_config_proto_msgTypes[17] + mi := &file_config_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1014,7 +1118,7 @@ func (x *SignalConfig) String() string { func (*SignalConfig) ProtoMessage() {} func (x *SignalConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[17] + mi := &file_config_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1027,7 +1131,7 @@ func (x *SignalConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SignalConfig.ProtoReflect.Descriptor instead. func (*SignalConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{17} + return file_config_proto_rawDescGZIP(), []int{19} } func (x *SignalConfig) GetAggregationConfig() *AggregationConfig { @@ -1062,7 +1166,7 @@ type AggregationConfig struct { func (x *AggregationConfig) Reset() { *x = AggregationConfig{} - mi := &file_config_proto_msgTypes[18] + mi := &file_config_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1074,7 +1178,7 @@ func (x *AggregationConfig) String() string { func (*AggregationConfig) ProtoMessage() {} func (x *AggregationConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[18] + mi := &file_config_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1087,7 +1191,7 @@ func (x *AggregationConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use AggregationConfig.ProtoReflect.Descriptor instead. func (*AggregationConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{18} + return file_config_proto_rawDescGZIP(), []int{20} } func (x *AggregationConfig) GetAlpha() float32 { @@ -1118,7 +1222,7 @@ type RoutineChangesConfig struct { func (x *RoutineChangesConfig) Reset() { *x = RoutineChangesConfig{} - mi := &file_config_proto_msgTypes[19] + mi := &file_config_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1130,7 +1234,7 @@ func (x *RoutineChangesConfig) String() string { func (*RoutineChangesConfig) ProtoMessage() {} func (x *RoutineChangesConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[19] + mi := &file_config_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1143,7 +1247,7 @@ func (x *RoutineChangesConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RoutineChangesConfig.ProtoReflect.Descriptor instead. func (*RoutineChangesConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{19} + return file_config_proto_rawDescGZIP(), []int{21} } func (x *RoutineChangesConfig) GetEventsPer() float32 { @@ -1190,7 +1294,7 @@ type GetGithubAppInformationRequest struct { func (x *GetGithubAppInformationRequest) Reset() { *x = GetGithubAppInformationRequest{} - mi := &file_config_proto_msgTypes[20] + mi := &file_config_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1202,7 +1306,7 @@ func (x *GetGithubAppInformationRequest) String() string { func (*GetGithubAppInformationRequest) ProtoMessage() {} func (x *GetGithubAppInformationRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[20] + mi := &file_config_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1215,7 +1319,7 @@ func (x *GetGithubAppInformationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGithubAppInformationRequest.ProtoReflect.Descriptor instead. func (*GetGithubAppInformationRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{20} + return file_config_proto_rawDescGZIP(), []int{22} } // it will be used to display information to the github integrations page, it is not used for signal processing @@ -1236,7 +1340,7 @@ type GithubAppInformation struct { func (x *GithubAppInformation) Reset() { *x = GithubAppInformation{} - mi := &file_config_proto_msgTypes[21] + mi := &file_config_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1248,7 +1352,7 @@ func (x *GithubAppInformation) String() string { func (*GithubAppInformation) ProtoMessage() {} func (x *GithubAppInformation) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[21] + mi := &file_config_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1261,7 +1365,7 @@ func (x *GithubAppInformation) ProtoReflect() protoreflect.Message { // Deprecated: Use GithubAppInformation.ProtoReflect.Descriptor instead. func (*GithubAppInformation) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{21} + return file_config_proto_rawDescGZIP(), []int{23} } func (x *GithubAppInformation) GetInstallationID() int64 { @@ -1337,7 +1441,7 @@ type GetGithubAppInformationResponse struct { func (x *GetGithubAppInformationResponse) Reset() { *x = GetGithubAppInformationResponse{} - mi := &file_config_proto_msgTypes[22] + mi := &file_config_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1349,7 +1453,7 @@ func (x *GetGithubAppInformationResponse) String() string { func (*GetGithubAppInformationResponse) ProtoMessage() {} func (x *GetGithubAppInformationResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[22] + mi := &file_config_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1362,7 +1466,7 @@ func (x *GetGithubAppInformationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGithubAppInformationResponse.ProtoReflect.Descriptor instead. func (*GetGithubAppInformationResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{22} + return file_config_proto_rawDescGZIP(), []int{24} } func (x *GetGithubAppInformationResponse) GetGithubAppInformation() *GithubAppInformation { @@ -1381,7 +1485,7 @@ type RegenerateGithubAppProfileRequest struct { func (x *RegenerateGithubAppProfileRequest) Reset() { *x = RegenerateGithubAppProfileRequest{} - mi := &file_config_proto_msgTypes[23] + mi := &file_config_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1393,7 +1497,7 @@ func (x *RegenerateGithubAppProfileRequest) String() string { func (*RegenerateGithubAppProfileRequest) ProtoMessage() {} func (x *RegenerateGithubAppProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[23] + mi := &file_config_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1406,7 +1510,7 @@ func (x *RegenerateGithubAppProfileRequest) ProtoReflect() protoreflect.Message // Deprecated: Use RegenerateGithubAppProfileRequest.ProtoReflect.Descriptor instead. func (*RegenerateGithubAppProfileRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{23} + return file_config_proto_rawDescGZIP(), []int{25} } // information stored in the database, used by signal processing in change analysis @@ -1421,7 +1525,7 @@ type GithubOrganisationProfile struct { func (x *GithubOrganisationProfile) Reset() { *x = GithubOrganisationProfile{} - mi := &file_config_proto_msgTypes[24] + mi := &file_config_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1433,7 +1537,7 @@ func (x *GithubOrganisationProfile) String() string { func (*GithubOrganisationProfile) ProtoMessage() {} func (x *GithubOrganisationProfile) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[24] + mi := &file_config_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1446,7 +1550,7 @@ func (x *GithubOrganisationProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use GithubOrganisationProfile.ProtoReflect.Descriptor instead. func (*GithubOrganisationProfile) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{24} + return file_config_proto_rawDescGZIP(), []int{26} } func (x *GithubOrganisationProfile) GetPrimaryBranchName() string { @@ -1472,7 +1576,7 @@ type RegenerateGithubAppProfileResponse struct { func (x *RegenerateGithubAppProfileResponse) Reset() { *x = RegenerateGithubAppProfileResponse{} - mi := &file_config_proto_msgTypes[25] + mi := &file_config_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1484,7 +1588,7 @@ func (x *RegenerateGithubAppProfileResponse) String() string { func (*RegenerateGithubAppProfileResponse) ProtoMessage() {} func (x *RegenerateGithubAppProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[25] + mi := &file_config_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1497,7 +1601,7 @@ func (x *RegenerateGithubAppProfileResponse) ProtoReflect() protoreflect.Message // Deprecated: Use RegenerateGithubAppProfileResponse.ProtoReflect.Descriptor instead. func (*RegenerateGithubAppProfileResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{25} + return file_config_proto_rawDescGZIP(), []int{27} } func (x *RegenerateGithubAppProfileResponse) GetGithubOrganisationProfile() *GithubOrganisationProfile { @@ -1516,7 +1620,7 @@ type DeleteGithubAppProfileAndGithubInstallationIDRequest struct { func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) Reset() { *x = DeleteGithubAppProfileAndGithubInstallationIDRequest{} - mi := &file_config_proto_msgTypes[26] + mi := &file_config_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1528,7 +1632,7 @@ func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) String() string { func (*DeleteGithubAppProfileAndGithubInstallationIDRequest) ProtoMessage() {} func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[26] + mi := &file_config_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1541,7 +1645,7 @@ func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) ProtoReflect() pr // Deprecated: Use DeleteGithubAppProfileAndGithubInstallationIDRequest.ProtoReflect.Descriptor instead. func (*DeleteGithubAppProfileAndGithubInstallationIDRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{26} + return file_config_proto_rawDescGZIP(), []int{28} } // status codes to indicate if the deletion was successful @@ -1553,7 +1657,7 @@ type DeleteGithubAppProfileAndGithubInstallationIDResponse struct { func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) Reset() { *x = DeleteGithubAppProfileAndGithubInstallationIDResponse{} - mi := &file_config_proto_msgTypes[27] + mi := &file_config_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1565,7 +1669,7 @@ func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) String() string func (*DeleteGithubAppProfileAndGithubInstallationIDResponse) ProtoMessage() {} func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[27] + mi := &file_config_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1578,7 +1682,7 @@ func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) ProtoReflect() p // Deprecated: Use DeleteGithubAppProfileAndGithubInstallationIDResponse.ProtoReflect.Descriptor instead. func (*DeleteGithubAppProfileAndGithubInstallationIDResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{27} + return file_config_proto_rawDescGZIP(), []int{29} } var File_config_proto protoreflect.FileDescriptor @@ -1625,7 +1729,12 @@ const file_config_proto_rawDesc = "" + "CONFIGURED\x10\x00\x12\t\n" + "\x05ERROR\x10\x01\"\x18\n" + "\x16DeleteHcpConfigRequest\"\x19\n" + - "\x17DeleteHcpConfigResponse\"\x18\n" + + "\x17DeleteHcpConfigResponse\"O\n" + + "\x17ReplaceHcpApiKeyRequest\x124\n" + + "\x15finalFrontendRedirect\x18\x01 \x01(\tR\x15finalFrontendRedirect\"|\n" + + "\x18ReplaceHcpApiKeyResponse\x12)\n" + + "\x06config\x18\x01 \x01(\v2\x11.config.HcpConfigR\x06config\x125\n" + + "\x06apiKey\x18\x02 \x01(\v2\x1d.apikeys.CreateAPIKeyResponseR\x06apiKey\"\x18\n" + "\x16GetSignalConfigRequest\"G\n" + "\x17GetSignalConfigResponse\x12,\n" + "\x06config\x18\x01 \x01(\v2\x14.config.SignalConfigR\x06config\"I\n" + @@ -1673,18 +1782,19 @@ const file_config_proto_rawDesc = "" + "\"RegenerateGithubAppProfileResponse\x12_\n" + "\x19githubOrganisationProfile\x18\x01 \x01(\v2!.config.GithubOrganisationProfileR\x19githubOrganisationProfile\"6\n" + "4DeleteGithubAppProfileAndGithubInstallationIDRequest\"7\n" + - "5DeleteGithubAppProfileAndGithubInstallationIDResponse2\x81\b\n" + + "5DeleteGithubAppProfileAndGithubInstallationIDResponse2\xd8\b\n" + "\x14ConfigurationService\x12U\n" + "\x10GetAccountConfig\x12\x1f.config.GetAccountConfigRequest\x1a .config.GetAccountConfigResponse\x12^\n" + "\x13UpdateAccountConfig\x12\".config.UpdateAccountConfigRequest\x1a#.config.UpdateAccountConfigResponse\x12R\n" + "\x0fCreateHcpConfig\x12\x1e.config.CreateHcpConfigRequest\x1a\x1f.config.CreateHcpConfigResponse\x12I\n" + "\fGetHcpConfig\x12\x1b.config.GetHcpConfigRequest\x1a\x1c.config.GetHcpConfigResponse\x12R\n" + - "\x0fDeleteHcpConfig\x12\x1e.config.DeleteHcpConfigRequest\x1a\x1f.config.DeleteHcpConfigResponse\x12R\n" + + "\x0fDeleteHcpConfig\x12\x1e.config.DeleteHcpConfigRequest\x1a\x1f.config.DeleteHcpConfigResponse\x12U\n" + + "\x10ReplaceHcpApiKey\x12\x1f.config.ReplaceHcpApiKeyRequest\x1a .config.ReplaceHcpApiKeyResponse\x12R\n" + "\x0fGetSignalConfig\x12\x1e.config.GetSignalConfigRequest\x1a\x1f.config.GetSignalConfigResponse\x12[\n" + "\x12UpdateSignalConfig\x12!.config.UpdateSignalConfigRequest\x1a\".config.UpdateSignalConfigResponse\x12j\n" + "\x17GetGithubAppInformation\x12&.config.GetGithubAppInformationRequest\x1a'.config.GetGithubAppInformationResponse\x12s\n" + "\x1aRegenerateGithubAppProfile\x12).config.RegenerateGithubAppProfileRequest\x1a*.config.RegenerateGithubAppProfileResponse\x12\xac\x01\n" + - "-DeleteGithubAppProfileAndGithubInstallationID\x12<.config.DeleteGithubAppProfileAndGithubInstallationIDRequest\x1a=.config.DeleteGithubAppProfileAndGithubInstallationIDResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "-DeleteGithubAppProfileAndGithubInstallationID\x12<.config.DeleteGithubAppProfileAndGithubInstallationIDRequest\x1a=.config.DeleteGithubAppProfileAndGithubInstallationIDResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_config_proto_rawDescOnce sync.Once @@ -1699,7 +1809,7 @@ func file_config_proto_rawDescGZIP() []byte { } var file_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_config_proto_goTypes = []any{ (AccountConfig_BlastRadiusPreset)(0), // 0: config.AccountConfig.BlastRadiusPreset (GetHcpConfigResponse_Status)(0), // 1: config.GetHcpConfigResponse.Status @@ -1717,72 +1827,78 @@ var file_config_proto_goTypes = []any{ (*GetHcpConfigResponse)(nil), // 13: config.GetHcpConfigResponse (*DeleteHcpConfigRequest)(nil), // 14: config.DeleteHcpConfigRequest (*DeleteHcpConfigResponse)(nil), // 15: config.DeleteHcpConfigResponse - (*GetSignalConfigRequest)(nil), // 16: config.GetSignalConfigRequest - (*GetSignalConfigResponse)(nil), // 17: config.GetSignalConfigResponse - (*UpdateSignalConfigRequest)(nil), // 18: config.UpdateSignalConfigRequest - (*UpdateSignalConfigResponse)(nil), // 19: config.UpdateSignalConfigResponse - (*SignalConfig)(nil), // 20: config.SignalConfig - (*AggregationConfig)(nil), // 21: config.AggregationConfig - (*RoutineChangesConfig)(nil), // 22: config.RoutineChangesConfig - (*GetGithubAppInformationRequest)(nil), // 23: config.GetGithubAppInformationRequest - (*GithubAppInformation)(nil), // 24: config.GithubAppInformation - (*GetGithubAppInformationResponse)(nil), // 25: config.GetGithubAppInformationResponse - (*RegenerateGithubAppProfileRequest)(nil), // 26: config.RegenerateGithubAppProfileRequest - (*GithubOrganisationProfile)(nil), // 27: config.GithubOrganisationProfile - (*RegenerateGithubAppProfileResponse)(nil), // 28: config.RegenerateGithubAppProfileResponse - (*DeleteGithubAppProfileAndGithubInstallationIDRequest)(nil), // 29: config.DeleteGithubAppProfileAndGithubInstallationIDRequest - (*DeleteGithubAppProfileAndGithubInstallationIDResponse)(nil), // 30: config.DeleteGithubAppProfileAndGithubInstallationIDResponse - (*durationpb.Duration)(nil), // 31: google.protobuf.Duration - (*CreateAPIKeyResponse)(nil), // 32: apikeys.CreateAPIKeyResponse - (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp + (*ReplaceHcpApiKeyRequest)(nil), // 16: config.ReplaceHcpApiKeyRequest + (*ReplaceHcpApiKeyResponse)(nil), // 17: config.ReplaceHcpApiKeyResponse + (*GetSignalConfigRequest)(nil), // 18: config.GetSignalConfigRequest + (*GetSignalConfigResponse)(nil), // 19: config.GetSignalConfigResponse + (*UpdateSignalConfigRequest)(nil), // 20: config.UpdateSignalConfigRequest + (*UpdateSignalConfigResponse)(nil), // 21: config.UpdateSignalConfigResponse + (*SignalConfig)(nil), // 22: config.SignalConfig + (*AggregationConfig)(nil), // 23: config.AggregationConfig + (*RoutineChangesConfig)(nil), // 24: config.RoutineChangesConfig + (*GetGithubAppInformationRequest)(nil), // 25: config.GetGithubAppInformationRequest + (*GithubAppInformation)(nil), // 26: config.GithubAppInformation + (*GetGithubAppInformationResponse)(nil), // 27: config.GetGithubAppInformationResponse + (*RegenerateGithubAppProfileRequest)(nil), // 28: config.RegenerateGithubAppProfileRequest + (*GithubOrganisationProfile)(nil), // 29: config.GithubOrganisationProfile + (*RegenerateGithubAppProfileResponse)(nil), // 30: config.RegenerateGithubAppProfileResponse + (*DeleteGithubAppProfileAndGithubInstallationIDRequest)(nil), // 31: config.DeleteGithubAppProfileAndGithubInstallationIDRequest + (*DeleteGithubAppProfileAndGithubInstallationIDResponse)(nil), // 32: config.DeleteGithubAppProfileAndGithubInstallationIDResponse + (*durationpb.Duration)(nil), // 33: google.protobuf.Duration + (*CreateAPIKeyResponse)(nil), // 34: apikeys.CreateAPIKeyResponse + (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp } var file_config_proto_depIdxs = []int32{ - 31, // 0: config.BlastRadiusConfig.changeAnalysisTargetDuration:type_name -> google.protobuf.Duration + 33, // 0: config.BlastRadiusConfig.changeAnalysisTargetDuration:type_name -> google.protobuf.Duration 0, // 1: config.AccountConfig.blastRadiusPreset:type_name -> config.AccountConfig.BlastRadiusPreset 3, // 2: config.AccountConfig.blastRadius:type_name -> config.BlastRadiusConfig 4, // 3: config.GetAccountConfigResponse.config:type_name -> config.AccountConfig 4, // 4: config.UpdateAccountConfigRequest.config:type_name -> config.AccountConfig 4, // 5: config.UpdateAccountConfigResponse.config:type_name -> config.AccountConfig 11, // 6: config.CreateHcpConfigResponse.config:type_name -> config.HcpConfig - 32, // 7: config.CreateHcpConfigResponse.apiKey:type_name -> apikeys.CreateAPIKeyResponse + 34, // 7: config.CreateHcpConfigResponse.apiKey:type_name -> apikeys.CreateAPIKeyResponse 11, // 8: config.GetHcpConfigResponse.config:type_name -> config.HcpConfig 1, // 9: config.GetHcpConfigResponse.status:type_name -> config.GetHcpConfigResponse.Status - 20, // 10: config.GetSignalConfigResponse.config:type_name -> config.SignalConfig - 20, // 11: config.UpdateSignalConfigRequest.config:type_name -> config.SignalConfig - 20, // 12: config.UpdateSignalConfigResponse.config:type_name -> config.SignalConfig - 21, // 13: config.SignalConfig.aggregationConfig:type_name -> config.AggregationConfig - 22, // 14: config.SignalConfig.routineChangesConfig:type_name -> config.RoutineChangesConfig - 27, // 15: config.SignalConfig.githubOrganisationProfile:type_name -> config.GithubOrganisationProfile - 2, // 16: config.RoutineChangesConfig.eventsPerUnit:type_name -> config.RoutineChangesConfig.DurationUnit - 2, // 17: config.RoutineChangesConfig.durationUnit:type_name -> config.RoutineChangesConfig.DurationUnit - 33, // 18: config.GithubAppInformation.installedAt:type_name -> google.protobuf.Timestamp - 24, // 19: config.GetGithubAppInformationResponse.githubAppInformation:type_name -> config.GithubAppInformation - 27, // 20: config.RegenerateGithubAppProfileResponse.githubOrganisationProfile:type_name -> config.GithubOrganisationProfile - 5, // 21: config.ConfigurationService.GetAccountConfig:input_type -> config.GetAccountConfigRequest - 7, // 22: config.ConfigurationService.UpdateAccountConfig:input_type -> config.UpdateAccountConfigRequest - 9, // 23: config.ConfigurationService.CreateHcpConfig:input_type -> config.CreateHcpConfigRequest - 12, // 24: config.ConfigurationService.GetHcpConfig:input_type -> config.GetHcpConfigRequest - 14, // 25: config.ConfigurationService.DeleteHcpConfig:input_type -> config.DeleteHcpConfigRequest - 16, // 26: config.ConfigurationService.GetSignalConfig:input_type -> config.GetSignalConfigRequest - 18, // 27: config.ConfigurationService.UpdateSignalConfig:input_type -> config.UpdateSignalConfigRequest - 23, // 28: config.ConfigurationService.GetGithubAppInformation:input_type -> config.GetGithubAppInformationRequest - 26, // 29: config.ConfigurationService.RegenerateGithubAppProfile:input_type -> config.RegenerateGithubAppProfileRequest - 29, // 30: config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID:input_type -> config.DeleteGithubAppProfileAndGithubInstallationIDRequest - 6, // 31: config.ConfigurationService.GetAccountConfig:output_type -> config.GetAccountConfigResponse - 8, // 32: config.ConfigurationService.UpdateAccountConfig:output_type -> config.UpdateAccountConfigResponse - 10, // 33: config.ConfigurationService.CreateHcpConfig:output_type -> config.CreateHcpConfigResponse - 13, // 34: config.ConfigurationService.GetHcpConfig:output_type -> config.GetHcpConfigResponse - 15, // 35: config.ConfigurationService.DeleteHcpConfig:output_type -> config.DeleteHcpConfigResponse - 17, // 36: config.ConfigurationService.GetSignalConfig:output_type -> config.GetSignalConfigResponse - 19, // 37: config.ConfigurationService.UpdateSignalConfig:output_type -> config.UpdateSignalConfigResponse - 25, // 38: config.ConfigurationService.GetGithubAppInformation:output_type -> config.GetGithubAppInformationResponse - 28, // 39: config.ConfigurationService.RegenerateGithubAppProfile:output_type -> config.RegenerateGithubAppProfileResponse - 30, // 40: config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID:output_type -> config.DeleteGithubAppProfileAndGithubInstallationIDResponse - 31, // [31:41] is the sub-list for method output_type - 21, // [21:31] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name + 11, // 10: config.ReplaceHcpApiKeyResponse.config:type_name -> config.HcpConfig + 34, // 11: config.ReplaceHcpApiKeyResponse.apiKey:type_name -> apikeys.CreateAPIKeyResponse + 22, // 12: config.GetSignalConfigResponse.config:type_name -> config.SignalConfig + 22, // 13: config.UpdateSignalConfigRequest.config:type_name -> config.SignalConfig + 22, // 14: config.UpdateSignalConfigResponse.config:type_name -> config.SignalConfig + 23, // 15: config.SignalConfig.aggregationConfig:type_name -> config.AggregationConfig + 24, // 16: config.SignalConfig.routineChangesConfig:type_name -> config.RoutineChangesConfig + 29, // 17: config.SignalConfig.githubOrganisationProfile:type_name -> config.GithubOrganisationProfile + 2, // 18: config.RoutineChangesConfig.eventsPerUnit:type_name -> config.RoutineChangesConfig.DurationUnit + 2, // 19: config.RoutineChangesConfig.durationUnit:type_name -> config.RoutineChangesConfig.DurationUnit + 35, // 20: config.GithubAppInformation.installedAt:type_name -> google.protobuf.Timestamp + 26, // 21: config.GetGithubAppInformationResponse.githubAppInformation:type_name -> config.GithubAppInformation + 29, // 22: config.RegenerateGithubAppProfileResponse.githubOrganisationProfile:type_name -> config.GithubOrganisationProfile + 5, // 23: config.ConfigurationService.GetAccountConfig:input_type -> config.GetAccountConfigRequest + 7, // 24: config.ConfigurationService.UpdateAccountConfig:input_type -> config.UpdateAccountConfigRequest + 9, // 25: config.ConfigurationService.CreateHcpConfig:input_type -> config.CreateHcpConfigRequest + 12, // 26: config.ConfigurationService.GetHcpConfig:input_type -> config.GetHcpConfigRequest + 14, // 27: config.ConfigurationService.DeleteHcpConfig:input_type -> config.DeleteHcpConfigRequest + 16, // 28: config.ConfigurationService.ReplaceHcpApiKey:input_type -> config.ReplaceHcpApiKeyRequest + 18, // 29: config.ConfigurationService.GetSignalConfig:input_type -> config.GetSignalConfigRequest + 20, // 30: config.ConfigurationService.UpdateSignalConfig:input_type -> config.UpdateSignalConfigRequest + 25, // 31: config.ConfigurationService.GetGithubAppInformation:input_type -> config.GetGithubAppInformationRequest + 28, // 32: config.ConfigurationService.RegenerateGithubAppProfile:input_type -> config.RegenerateGithubAppProfileRequest + 31, // 33: config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID:input_type -> config.DeleteGithubAppProfileAndGithubInstallationIDRequest + 6, // 34: config.ConfigurationService.GetAccountConfig:output_type -> config.GetAccountConfigResponse + 8, // 35: config.ConfigurationService.UpdateAccountConfig:output_type -> config.UpdateAccountConfigResponse + 10, // 36: config.ConfigurationService.CreateHcpConfig:output_type -> config.CreateHcpConfigResponse + 13, // 37: config.ConfigurationService.GetHcpConfig:output_type -> config.GetHcpConfigResponse + 15, // 38: config.ConfigurationService.DeleteHcpConfig:output_type -> config.DeleteHcpConfigResponse + 17, // 39: config.ConfigurationService.ReplaceHcpApiKey:output_type -> config.ReplaceHcpApiKeyResponse + 19, // 40: config.ConfigurationService.GetSignalConfig:output_type -> config.GetSignalConfigResponse + 21, // 41: config.ConfigurationService.UpdateSignalConfig:output_type -> config.UpdateSignalConfigResponse + 27, // 42: config.ConfigurationService.GetGithubAppInformation:output_type -> config.GetGithubAppInformationResponse + 30, // 43: config.ConfigurationService.RegenerateGithubAppProfile:output_type -> config.RegenerateGithubAppProfileResponse + 32, // 44: config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID:output_type -> config.DeleteGithubAppProfileAndGithubInstallationIDResponse + 34, // [34:45] is the sub-list for method output_type + 23, // [23:34] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name } func init() { file_config_proto_init() } @@ -1793,14 +1909,14 @@ func file_config_proto_init() { file_apikeys_proto_init() file_config_proto_msgTypes[0].OneofWrappers = []any{} file_config_proto_msgTypes[1].OneofWrappers = []any{} - file_config_proto_msgTypes[17].OneofWrappers = []any{} + file_config_proto_msgTypes[19].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), NumEnums: 3, - NumMessages: 28, + NumMessages: 30, NumExtensions: 0, NumServices: 1, }, diff --git a/sdp-go/connection.go b/go/sdp-go/connection.go similarity index 100% rename from sdp-go/connection.go rename to go/sdp-go/connection.go diff --git a/sdp-go/connection_test.go b/go/sdp-go/connection_test.go similarity index 100% rename from sdp-go/connection_test.go rename to go/sdp-go/connection_test.go diff --git a/sdp-go/encoder_test.go b/go/sdp-go/encoder_test.go similarity index 100% rename from sdp-go/encoder_test.go rename to go/sdp-go/encoder_test.go diff --git a/sdp-go/errors.go b/go/sdp-go/errors.go similarity index 100% rename from sdp-go/errors.go rename to go/sdp-go/errors.go diff --git a/sdp-go/gateway.go b/go/sdp-go/gateway.go similarity index 100% rename from sdp-go/gateway.go rename to go/sdp-go/gateway.go diff --git a/sdp-go/gateway.pb.go b/go/sdp-go/gateway.pb.go similarity index 99% rename from sdp-go/gateway.pb.go rename to go/sdp-go/gateway.pb.go index 66435b0f..3728f320 100644 --- a/sdp-go/gateway.pb.go +++ b/go/sdp-go/gateway.pb.go @@ -2167,7 +2167,7 @@ const file_gateway_proto_rawDesc = "" + "\ttool_type\"8\n" + "\fChatResponse\x12\x12\n" + "\x04text\x18\x01 \x01(\tR\x04text\x12\x14\n" + - "\x05error\x18\x02 \x01(\tR\x05errorB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x05error\x18\x02 \x01(\tR\x05errorB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_gateway_proto_rawDescOnce sync.Once diff --git a/sdp-go/gateway_test.go b/go/sdp-go/gateway_test.go similarity index 100% rename from sdp-go/gateway_test.go rename to go/sdp-go/gateway_test.go diff --git a/sdp-go/genhandler.go b/go/sdp-go/genhandler.go similarity index 98% rename from sdp-go/genhandler.go rename to go/sdp-go/genhandler.go index bc0c4224..24025a74 100644 --- a/sdp-go/genhandler.go +++ b/go/sdp-go/genhandler.go @@ -42,7 +42,7 @@ import ( "github.com/nats-io/nats.go" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) func New{{.Type}}Handler(spanName string, h func(ctx context.Context, i *{{.Type}}), spanOpts ...trace.SpanStartOption) nats.MsgHandler { diff --git a/sdp-go/graph/main.go b/go/sdp-go/graph/main.go similarity index 99% rename from sdp-go/graph/main.go rename to go/sdp-go/graph/main.go index ab04ab91..ee6d4838 100644 --- a/sdp-go/graph/main.go +++ b/go/sdp-go/graph/main.go @@ -6,7 +6,7 @@ package graph import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/set/uid" ) diff --git a/sdp-go/graph/main_test.go b/go/sdp-go/graph/main_test.go similarity index 99% rename from sdp-go/graph/main_test.go rename to go/sdp-go/graph/main_test.go index b16d14b7..12500713 100644 --- a/sdp-go/graph/main_test.go +++ b/go/sdp-go/graph/main_test.go @@ -3,7 +3,7 @@ package graph import ( "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "gonum.org/v1/gonum/graph/network" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/sdp-go/handler_cancelquery.go b/go/sdp-go/handler_cancelquery.go similarity index 96% rename from sdp-go/handler_cancelquery.go rename to go/sdp-go/handler_cancelquery.go index 921a58dc..4a3631fd 100644 --- a/sdp-go/handler_cancelquery.go +++ b/go/sdp-go/handler_cancelquery.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) func NewCancelQueryHandler(spanName string, h func(ctx context.Context, i *CancelQuery), spanOpts ...trace.SpanStartOption) nats.MsgHandler { diff --git a/sdp-go/handler_gatewayresponse.go b/go/sdp-go/handler_gatewayresponse.go similarity index 96% rename from sdp-go/handler_gatewayresponse.go rename to go/sdp-go/handler_gatewayresponse.go index 776f22bd..a421a06e 100644 --- a/sdp-go/handler_gatewayresponse.go +++ b/go/sdp-go/handler_gatewayresponse.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) func NewGatewayResponseHandler(spanName string, h func(ctx context.Context, i *GatewayResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { diff --git a/sdp-go/handler_natsgetlogrecordsrequest.go b/go/sdp-go/handler_natsgetlogrecordsrequest.go similarity index 97% rename from sdp-go/handler_natsgetlogrecordsrequest.go rename to go/sdp-go/handler_natsgetlogrecordsrequest.go index 64e08127..97628de6 100644 --- a/sdp-go/handler_natsgetlogrecordsrequest.go +++ b/go/sdp-go/handler_natsgetlogrecordsrequest.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) func NewNATSGetLogRecordsRequestHandler(spanName string, h func(ctx context.Context, i *NATSGetLogRecordsRequest), spanOpts ...trace.SpanStartOption) nats.MsgHandler { diff --git a/sdp-go/handler_natsgetlogrecordsresponse.go b/go/sdp-go/handler_natsgetlogrecordsresponse.go similarity index 97% rename from sdp-go/handler_natsgetlogrecordsresponse.go rename to go/sdp-go/handler_natsgetlogrecordsresponse.go index 1af8bb5e..c2f24d4d 100644 --- a/sdp-go/handler_natsgetlogrecordsresponse.go +++ b/go/sdp-go/handler_natsgetlogrecordsresponse.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) func NewNATSGetLogRecordsResponseHandler(spanName string, h func(ctx context.Context, i *NATSGetLogRecordsResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { diff --git a/sdp-go/handler_query.go b/go/sdp-go/handler_query.go similarity index 96% rename from sdp-go/handler_query.go rename to go/sdp-go/handler_query.go index 3bd8adca..44c78b02 100644 --- a/sdp-go/handler_query.go +++ b/go/sdp-go/handler_query.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) func NewQueryHandler(spanName string, h func(ctx context.Context, i *Query), spanOpts ...trace.SpanStartOption) nats.MsgHandler { diff --git a/sdp-go/handler_queryresponse.go b/go/sdp-go/handler_queryresponse.go similarity index 96% rename from sdp-go/handler_queryresponse.go rename to go/sdp-go/handler_queryresponse.go index 2f8c309c..f629818e 100644 --- a/sdp-go/handler_queryresponse.go +++ b/go/sdp-go/handler_queryresponse.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) func NewQueryResponseHandler(spanName string, h func(ctx context.Context, i *QueryResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { diff --git a/sdp-go/instance_detect.go b/go/sdp-go/instance_detect.go similarity index 98% rename from sdp-go/instance_detect.go rename to go/sdp-go/instance_detect.go index deb2ef56..2da3ac9e 100644 --- a/sdp-go/instance_detect.go +++ b/go/sdp-go/instance_detect.go @@ -7,7 +7,7 @@ import ( "net/http" "net/url" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) // Information about a particular instance of Overmind. This is used to diff --git a/sdp-go/invites.pb.go b/go/sdp-go/invites.pb.go similarity index 99% rename from sdp-go/invites.pb.go rename to go/sdp-go/invites.pb.go index 72b2ec9f..c7b63a1e 100644 --- a/sdp-go/invites.pb.go +++ b/go/sdp-go/invites.pb.go @@ -474,7 +474,7 @@ const file_invites_proto_rawDesc = "" + "\fCreateInvite\x12\x1c.invites.CreateInviteRequest\x1a\x1d.invites.CreateInviteResponse\x12H\n" + "\vListInvites\x12\x1b.invites.ListInvitesRequest\x1a\x1c.invites.ListInvitesResponse\x12K\n" + "\fRevokeInvite\x12\x1c.invites.RevokeInviteRequest\x1a\x1d.invites.RevokeInviteResponse\x12K\n" + - "\fResendInvite\x12\x1c.invites.ResendInviteRequest\x1a\x1d.invites.ResendInviteResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\fResendInvite\x12\x1c.invites.ResendInviteRequest\x1a\x1d.invites.ResendInviteResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_invites_proto_rawDescOnce sync.Once diff --git a/sdp-go/items.go b/go/sdp-go/items.go similarity index 93% rename from sdp-go/items.go rename to go/sdp-go/items.go index 8f09d3c9..49f7f59b 100644 --- a/sdp-go/items.go +++ b/go/sdp-go/items.go @@ -20,10 +20,6 @@ import ( const WILDCARD = "*" -func (bp *BlastPropagation) IsEqual(other *BlastPropagation) bool { - return bp.GetIn() == other.GetIn() && bp.GetOut() == other.GetOut() -} - // UniqueAttributeValue returns the value of whatever the Unique Attribute is // for this item. This will then be converted to a string and returned func (i *Item) UniqueAttributeValue() string { @@ -72,10 +68,11 @@ func (i *Item) Hash() string { return HashSum(([]byte(fmt.Sprint(i.GloballyUniqueName())))) } +// IsEqual compares two Edges for equality by checking the From reference +// and To reference. func (e *Edge) IsEqual(other *Edge) bool { return e.GetFrom().IsEqual(other.GetFrom()) && - e.GetTo().IsEqual(other.GetTo()) && - e.GetBlastPropagation().IsEqual(other.GetBlastPropagation()) + e.GetTo().IsEqual(other.GetTo()) } // Hash Returns a 12 character hash for the item. This is likely but not @@ -140,6 +137,8 @@ func (r *Reference) IsSingle() bool { return true } +// IsEqual compares two References for equality by checking all fields: +// Scope, Type, UniqueAttributeValue, IsQuery, Method, and Query. func (r *Reference) IsEqual(other *Reference) bool { return r.GetScope() == other.GetScope() && r.GetType() == other.GetType() && @@ -149,6 +148,9 @@ func (r *Reference) IsEqual(other *Reference) bool { r.GetQuery() == other.GetQuery() } +// ToQuery converts a Reference to a Query object. If the Reference is not +// already a query (IsQuery=false), it creates a GET query using the +// UniqueAttributeValue. Otherwise, it preserves the existing query parameters. func (r *Reference) ToQuery() *Query { if !r.GetIsQuery() { return &Query{ @@ -275,6 +277,9 @@ func (r *Query) GetUUIDParsed() uuid.UUID { return reqUUID } +// SetSpanAttributes sets OpenTelemetry span attributes for the query, +// including method, type, scope, query string, UUID, deadline, and cache settings. +// All attributes are prefixed with "ovm.sdp." for namespacing. func (q *Query) SetSpanAttributes(span trace.Span) { span.SetAttributes( attribute.String("ovm.sdp.method", q.GetMethod().String()), @@ -287,6 +292,7 @@ func (q *Query) SetSpanAttributes(span trace.Span) { ) } +// NewQueryResponseFromItem creates a QueryResponse wrapping a discovered Item. func NewQueryResponseFromItem(item *Item) *QueryResponse { return &QueryResponse{ ResponseType: &QueryResponse_NewItem{ @@ -295,6 +301,7 @@ func NewQueryResponseFromItem(item *Item) *QueryResponse { } } +// NewQueryResponseFromEdge creates a QueryResponse wrapping a discovered Edge. func NewQueryResponseFromEdge(edge *Edge) *QueryResponse { return &QueryResponse{ ResponseType: &QueryResponse_Edge{ @@ -303,6 +310,7 @@ func NewQueryResponseFromEdge(edge *Edge) *QueryResponse { } } +// NewQueryResponseFromError creates a QueryResponse wrapping a QueryError. func NewQueryResponseFromError(qe *QueryError) *QueryResponse { return &QueryResponse{ ResponseType: &QueryResponse_Error{ @@ -311,6 +319,7 @@ func NewQueryResponseFromError(qe *QueryError) *QueryResponse { } } +// NewQueryResponseFromResponse creates a QueryResponse wrapping a Response status update. func NewQueryResponseFromResponse(r *Response) *QueryResponse { return &QueryResponse{ ResponseType: &QueryResponse_Response{ @@ -319,6 +328,8 @@ func NewQueryResponseFromResponse(r *Response) *QueryResponse { } } +// ToGatewayResponse converts a QueryResponse to a GatewayResponse for sending +// to clients. Handles Item, Edge, Error, and Response status types. func (qr *QueryResponse) ToGatewayResponse() *GatewayResponse { switch qr.GetResponseType().(type) { case *QueryResponse_NewItem: @@ -350,6 +361,7 @@ func (qr *QueryResponse) ToGatewayResponse() *GatewayResponse { } } +// GetUUIDParsed returns the parsed UUID from the CancelQuery, or nil if invalid. func (x *CancelQuery) GetUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(x.GetUUID()) if err != nil { @@ -358,6 +370,7 @@ func (x *CancelQuery) GetUUIDParsed() *uuid.UUID { return &u } +// GetUUIDParsed returns the parsed UUID from the Expand request, or nil if invalid. func (x *Expand) GetUUIDParsed() *uuid.UUID { u, err := uuid.FromBytes(x.GetUUID()) if err != nil { diff --git a/sdp-go/items.pb.go b/go/sdp-go/items.pb.go similarity index 88% rename from sdp-go/items.pb.go rename to go/sdp-go/items.pb.go index ca8305fd..f05460e2 100644 --- a/sdp-go/items.pb.go +++ b/go/sdp-go/items.pb.go @@ -278,27 +278,13 @@ func (QueryError_ErrorType) EnumDescriptor() ([]byte, []int) { return file_items_proto_rawDescGZIP(), []int{11, 0} } -// This message stores additional information on Edges (and edge-like constructs) to determine how configuration changes can impact -// the linked items. +// DEPRECATED: BlastPropagation was previously used to determine how configuration +// changes propagate over links. It has been replaced with an AI-driven approach +// for blast radius calculation and is no longer used. // -// Blast Propagation options: -// -// |-------|-------|---------------------- -// | in | out | result -// |-------|-------|---------------------- -// | false | false | no change in any item can affect the other -// | false | true | a change to this item can affect its linked items -// | | | example: a change to an EC2 instance can affect its DNS name (in the sense that other items depending on that DNS name will see the impact) -// | true | false | a change to linked items can affect this item -// | | | example: changing the KMS key used by a DynamoDB table can impact the table, but no change to the table can impact the key -// | true | true | changes on both sides of the link can affect the other -// | | | example: changes to both EC2 Instances and their volumes can affect the other side of the relation. +// Reserved to prevent field number reuse and maintain wire-format compatibility. type BlastPropagation struct { - state protoimpl.MessageState `protogen:"open.v1"` - // is true if changes on linked items can affect this item - In bool `protobuf:"varint,1,opt,name=in,proto3" json:"in,omitempty"` - // is true if changes on this item can affect linked items - Out bool `protobuf:"varint,2,opt,name=out,proto3" json:"out,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -333,29 +319,13 @@ func (*BlastPropagation) Descriptor() ([]byte, []int) { return file_items_proto_rawDescGZIP(), []int{0} } -func (x *BlastPropagation) GetIn() bool { - if x != nil { - return x.In - } - return false -} - -func (x *BlastPropagation) GetOut() bool { - if x != nil { - return x.Out - } - return false -} - // An annotated query to indicate potential linked items. type LinkedItemQuery struct { state protoimpl.MessageState `protogen:"open.v1"` // the query that would find linked items - Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` - // how configuration changes (i.e. the "blast") propagates over this link - BlastPropagation *BlastPropagation `protobuf:"bytes,2,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LinkedItemQuery) Reset() { @@ -395,22 +365,13 @@ func (x *LinkedItemQuery) GetQuery() *Query { return nil } -func (x *LinkedItemQuery) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - // An annotated reference to list linked items. type LinkedItem struct { state protoimpl.MessageState `protogen:"open.v1"` // the linked item - Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - // how configuration changes (i.e. the "blast") propagates over this link - BlastPropagation *BlastPropagation `protobuf:"bytes,2,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LinkedItem) Reset() { @@ -450,13 +411,6 @@ func (x *LinkedItem) GetItem() *Reference { return nil } -func (x *LinkedItem) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - // This is the same as Item within the package with a couple of exceptions, no // real reason why this whole thing couldn't be modelled in protobuf though if // required. Just need to decide what if anything should remain private @@ -1477,12 +1431,11 @@ func (x *Reference) GetMethod() QueryMethod { // that will be unrolled by the gateway during query processing. Clients are // guaranteed that edges are only sent after the referenced items. type Edge struct { - state protoimpl.MessageState `protogen:"open.v1"` - From *Reference `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` - To *Reference `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` - BlastPropagation *BlastPropagation `protobuf:"bytes,3,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + From *Reference `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` + To *Reference `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Edge) Reset() { @@ -1529,13 +1482,6 @@ func (x *Edge) GetTo() *Reference { return nil } -func (x *Edge) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - // Defines how this query should behave when finding new items type Query_RecursionBehaviour struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1543,11 +1489,9 @@ type Query_RecursionBehaviour struct { // To resolve linked items "infinitely" simply set this to a high number, with // the highest being 4,294,967,295. While this isn't truly *infinite*, chances // are that it is effectively the same, think six degrees of separation etc. - LinkDepth uint32 `protobuf:"varint,1,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` - // set to true to only follow links that propagate configuration change impact - FollowOnlyBlastPropagation bool `protobuf:"varint,2,opt,name=followOnlyBlastPropagation,proto3" json:"followOnlyBlastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + LinkDepth uint32 `protobuf:"varint,1,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Query_RecursionBehaviour) Reset() { @@ -1587,29 +1531,18 @@ func (x *Query_RecursionBehaviour) GetLinkDepth() uint32 { return 0 } -func (x *Query_RecursionBehaviour) GetFollowOnlyBlastPropagation() bool { - if x != nil { - return x.FollowOnlyBlastPropagation - } - return false -} - var File_items_proto protoreflect.FileDescriptor const file_items_proto_rawDesc = "" + "\n" + - "\vitems.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fresponses.proto\"4\n" + - "\x10BlastPropagation\x12\x0e\n" + - "\x02in\x18\x01 \x01(\bR\x02in\x12\x10\n" + - "\x03out\x18\x02 \x01(\bR\x03out\"n\n" + + "\vitems.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fresponses.proto\"'\n" + + "\x10BlastPropagationJ\x04\b\x01\x10\x02J\x04\b\x02\x10\x03R\x02inR\x03out\"G\n" + "\x0fLinkedItemQuery\x12\x1c\n" + - "\x05query\x18\x01 \x01(\v2\x06.QueryR\x05query\x12=\n" + - "\x10blastPropagation\x18\x02 \x01(\v2\x11.BlastPropagationR\x10blastPropagation\"k\n" + + "\x05query\x18\x01 \x01(\v2\x06.QueryR\x05queryJ\x04\b\x02\x10\x03R\x10blastPropagation\"D\n" + "\n" + "LinkedItem\x12\x1e\n" + "\x04item\x18\x01 \x01(\v2\n" + - ".ReferenceR\x04item\x12=\n" + - "\x10blastPropagation\x18\x02 \x01(\v2\x11.BlastPropagationR\x10blastPropagation\"\xe3\x03\n" + + ".ReferenceR\x04itemJ\x04\b\x02\x10\x03R\x10blastPropagation\"\xe3\x03\n" + "\x04Item\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12(\n" + "\x0funiqueAttribute\x18\x02 \x01(\tR\x0funiqueAttribute\x12/\n" + @@ -1647,7 +1580,7 @@ const file_items_proto_rawDesc = "" + "\x10LogStreamDetails\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05scope\x18\x02 \x01(\tR\x05scope\x12\x14\n" + - "\x05query\x18\x03 \x01(\tR\x05query\"\xa0\x03\n" + + "\x05query\x18\x03 \x01(\tR\x05query\"\x82\x03\n" + "\x05Query\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12$\n" + "\x06method\x18\x02 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + @@ -1656,10 +1589,9 @@ const file_items_proto_rawDesc = "" + "\x05scope\x18\x05 \x01(\tR\x05scope\x12 \n" + "\vignoreCache\x18\x06 \x01(\bR\vignoreCache\x12\x12\n" + "\x04UUID\x18\a \x01(\fR\x04UUID\x126\n" + - "\bdeadline\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\x1ar\n" + + "\bdeadline\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\x1aT\n" + "\x12RecursionBehaviour\x12\x1c\n" + - "\tlinkDepth\x18\x01 \x01(\rR\tlinkDepth\x12>\n" + - "\x1afollowOnlyBlastPropagation\x18\x02 \x01(\bR\x1afollowOnlyBlastPropagationJ\x04\b\b\x10\t\"\xae\x01\n" + + "\tlinkDepth\x18\x01 \x01(\rR\tlinkDepthJ\x04\b\x02\x10\x03R\x1afollowOnlyBlastPropagationJ\x04\b\b\x10\t\"\xae\x01\n" + "\rQueryResponse\x12!\n" + "\anewItem\x18\x02 \x01(\v2\x05.ItemH\x00R\anewItem\x12'\n" + "\bresponse\x18\x03 \x01(\v2\t.ResponseH\x00R\bresponse\x12#\n" + @@ -1705,13 +1637,12 @@ const file_items_proto_rawDesc = "" + "\x05scope\x18\x03 \x01(\tR\x05scope\x12\x18\n" + "\aisQuery\x18\x04 \x01(\bR\aisQuery\x12\x14\n" + "\x05query\x18\x05 \x01(\tR\x05query\x12$\n" + - "\x06method\x18\x06 \x01(\x0e2\f.QueryMethodR\x06method\"\x81\x01\n" + + "\x06method\x18\x06 \x01(\x0e2\f.QueryMethodR\x06method\"Z\n" + "\x04Edge\x12\x1e\n" + "\x04from\x18\x01 \x01(\v2\n" + ".ReferenceR\x04from\x12\x1a\n" + "\x02to\x18\x02 \x01(\v2\n" + - ".ReferenceR\x02to\x12=\n" + - "\x10blastPropagation\x18\x03 \x01(\v2\x11.BlastPropagationR\x10blastPropagation*e\n" + + ".ReferenceR\x02toJ\x04\b\x03\x10\x04R\x10blastPropagation*e\n" + "\x06Health\x12\x12\n" + "\x0eHEALTH_UNKNOWN\x10\x00\x12\r\n" + "\tHEALTH_OK\x10\x01\x12\x12\n" + @@ -1722,7 +1653,7 @@ const file_items_proto_rawDesc = "" + "\x03GET\x10\x00\x12\b\n" + "\x04LIST\x10\x01\x12\n" + "\n" + - "\x06SEARCH\x10\x02B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x06SEARCH\x10\x02B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_items_proto_rawDescOnce sync.Once @@ -1768,42 +1699,39 @@ var file_items_proto_goTypes = []any{ } var file_items_proto_depIdxs = []int32{ 12, // 0: LinkedItemQuery.query:type_name -> Query - 4, // 1: LinkedItemQuery.blastPropagation:type_name -> BlastPropagation - 18, // 2: LinkedItem.item:type_name -> Reference - 4, // 3: LinkedItem.blastPropagation:type_name -> BlastPropagation - 8, // 4: Item.attributes:type_name -> ItemAttributes - 9, // 5: Item.metadata:type_name -> Metadata - 5, // 6: Item.linkedItemQueries:type_name -> LinkedItemQuery - 6, // 7: Item.linkedItems:type_name -> LinkedItem - 0, // 8: Item.health:type_name -> Health - 20, // 9: Item.tags:type_name -> Item.TagsEntry - 11, // 10: Item.logStreams:type_name -> LogStreamDetails - 22, // 11: ItemAttributes.attrStruct:type_name -> google.protobuf.Struct - 12, // 12: Metadata.sourceQuery:type_name -> Query - 23, // 13: Metadata.timestamp:type_name -> google.protobuf.Timestamp - 24, // 14: Metadata.sourceDuration:type_name -> google.protobuf.Duration - 24, // 15: Metadata.sourceDurationPerItem:type_name -> google.protobuf.Duration - 7, // 16: Items.items:type_name -> Item - 1, // 17: Query.method:type_name -> QueryMethod - 21, // 18: Query.recursionBehaviour:type_name -> Query.RecursionBehaviour - 23, // 19: Query.deadline:type_name -> google.protobuf.Timestamp - 7, // 20: QueryResponse.newItem:type_name -> Item - 25, // 21: QueryResponse.response:type_name -> Response - 15, // 22: QueryResponse.error:type_name -> QueryError - 19, // 23: QueryResponse.edge:type_name -> Edge - 2, // 24: QueryStatus.status:type_name -> QueryStatus.Status - 3, // 25: QueryError.errorType:type_name -> QueryError.ErrorType - 18, // 26: Expand.item:type_name -> Reference - 23, // 27: Expand.deadline:type_name -> google.protobuf.Timestamp - 1, // 28: Reference.method:type_name -> QueryMethod - 18, // 29: Edge.from:type_name -> Reference - 18, // 30: Edge.to:type_name -> Reference - 4, // 31: Edge.blastPropagation:type_name -> BlastPropagation - 32, // [32:32] is the sub-list for method output_type - 32, // [32:32] is the sub-list for method input_type - 32, // [32:32] is the sub-list for extension type_name - 32, // [32:32] is the sub-list for extension extendee - 0, // [0:32] is the sub-list for field type_name + 18, // 1: LinkedItem.item:type_name -> Reference + 8, // 2: Item.attributes:type_name -> ItemAttributes + 9, // 3: Item.metadata:type_name -> Metadata + 5, // 4: Item.linkedItemQueries:type_name -> LinkedItemQuery + 6, // 5: Item.linkedItems:type_name -> LinkedItem + 0, // 6: Item.health:type_name -> Health + 20, // 7: Item.tags:type_name -> Item.TagsEntry + 11, // 8: Item.logStreams:type_name -> LogStreamDetails + 22, // 9: ItemAttributes.attrStruct:type_name -> google.protobuf.Struct + 12, // 10: Metadata.sourceQuery:type_name -> Query + 23, // 11: Metadata.timestamp:type_name -> google.protobuf.Timestamp + 24, // 12: Metadata.sourceDuration:type_name -> google.protobuf.Duration + 24, // 13: Metadata.sourceDurationPerItem:type_name -> google.protobuf.Duration + 7, // 14: Items.items:type_name -> Item + 1, // 15: Query.method:type_name -> QueryMethod + 21, // 16: Query.recursionBehaviour:type_name -> Query.RecursionBehaviour + 23, // 17: Query.deadline:type_name -> google.protobuf.Timestamp + 7, // 18: QueryResponse.newItem:type_name -> Item + 25, // 19: QueryResponse.response:type_name -> Response + 15, // 20: QueryResponse.error:type_name -> QueryError + 19, // 21: QueryResponse.edge:type_name -> Edge + 2, // 22: QueryStatus.status:type_name -> QueryStatus.Status + 3, // 23: QueryError.errorType:type_name -> QueryError.ErrorType + 18, // 24: Expand.item:type_name -> Reference + 23, // 25: Expand.deadline:type_name -> google.protobuf.Timestamp + 1, // 26: Reference.method:type_name -> QueryMethod + 18, // 27: Edge.from:type_name -> Reference + 18, // 28: Edge.to:type_name -> Reference + 29, // [29:29] is the sub-list for method output_type + 29, // [29:29] is the sub-list for method input_type + 29, // [29:29] is the sub-list for extension type_name + 29, // [29:29] is the sub-list for extension extendee + 0, // [0:29] is the sub-list for field type_name } func init() { file_items_proto_init() } diff --git a/sdp-go/items_test.go b/go/sdp-go/items_test.go similarity index 100% rename from sdp-go/items_test.go rename to go/sdp-go/items_test.go diff --git a/sdp-go/link_extract.go b/go/sdp-go/link_extract.go similarity index 92% rename from sdp-go/link_extract.go rename to go/sdp-go/link_extract.go index d0e6c383..9f03b99f 100644 --- a/sdp-go/link_extract.go +++ b/go/sdp-go/link_extract.go @@ -96,10 +96,6 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { Query: ip.String(), Scope: "global", }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: true, - }, }, } } @@ -118,15 +114,6 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { Query: val, Scope: "global", }, - BlastPropagation: &BlastPropagation{ - // If we are referencing a HTTP URL, I think it's safe - // to assume that this is something that the current - // resource depends on and therefore that the blast - // radius should propagate inwards. This is a bit of a - // guess though... - In: true, - Out: false, - }, }, } } @@ -145,10 +132,6 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { Query: val, Scope: "global", }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: false, - }, }, } } @@ -214,10 +197,6 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { Query: val, Scope: scope, }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: false, - }, }, } } diff --git a/sdp-go/link_extract_test.go b/go/sdp-go/link_extract_test.go similarity index 100% rename from sdp-go/link_extract_test.go rename to go/sdp-go/link_extract_test.go diff --git a/sdp-go/logs.go b/go/sdp-go/logs.go similarity index 100% rename from sdp-go/logs.go rename to go/sdp-go/logs.go diff --git a/sdp-go/logs.pb.go b/go/sdp-go/logs.pb.go similarity index 99% rename from sdp-go/logs.pb.go rename to go/sdp-go/logs.pb.go index 2ae943fc..1d3a2d54 100644 --- a/sdp-go/logs.pb.go +++ b/go/sdp-go/logs.pb.go @@ -997,7 +997,7 @@ const file_logs_proto_rawDesc = "" + "\n" + "\x06FATAL4\x10\x182Y\n" + "\vLogsService\x12J\n" + - "\rGetLogRecords\x12\x1a.logs.GetLogRecordsRequest\x1a\x1b.logs.GetLogRecordsResponse0\x01B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\rGetLogRecords\x12\x1a.logs.GetLogRecordsRequest\x1a\x1b.logs.GetLogRecordsResponse0\x01B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_logs_proto_rawDescOnce sync.Once diff --git a/sdp-go/logs_test.go b/go/sdp-go/logs_test.go similarity index 100% rename from sdp-go/logs_test.go rename to go/sdp-go/logs_test.go diff --git a/sdp-go/progress.go b/go/sdp-go/progress.go similarity index 94% rename from sdp-go/progress.go rename to go/sdp-go/progress.go index e009fcd8..f53c5094 100644 --- a/sdp-go/progress.go +++ b/go/sdp-go/progress.go @@ -11,7 +11,7 @@ import ( "github.com/getsentry/sentry-go" "github.com/google/uuid" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/types/known/durationpb" @@ -237,7 +237,16 @@ func (l *lastResponse) checkStalled() { } } -// SourceQuery represents the status of a query +// SourceQuery tracks the progress of a query across multiple responders (Sources). +// It manages a state machine for each responder with the following states: +// +// WORKING → COMPLETE (normal completion) +// WORKING → ERROR (responder failed) +// WORKING → CANCELLED (query was cancelled) +// WORKING → STALLED (responder stopped sending updates) +// +// A query is considered finished when the start timeout has elapsed AND all +// responders are in a terminal state (COMPLETE, ERROR, CANCELLED, or STALLED). type SourceQuery struct { // A map of ResponderUUIDs to the last response we got from them responders map[uuid.UUID]*lastResponse @@ -256,7 +265,8 @@ type SourceQuery struct { cancel context.CancelFunc } -// The current progress of the tracked query +// SourceQueryProgress represents the current progress of a tracked query, +// aggregating the state of all responders. type SourceQueryProgress struct { // How many responders are currently working on this query. This means they // are active sending updates @@ -469,9 +479,9 @@ func RunSourceQuerySync(ctx context.Context, query *Query, startTimeout time.Dur return items, edges, errs, nil } -// Cancels the request, sending a cancel message to all responders and closing -// the response channel. The query can also be cancelled by cancelling the -// context that was passed in the `Start` method +// Cancel cancels the query by sending a cancel message to all responders and +// closing the response channel. Alternatively, the query can be cancelled by +// cancelling the context passed to RunSourceQuery. func (sq *SourceQuery) Cancel() { sq.cancel() } @@ -654,23 +664,23 @@ func TranslateLinksToEdges(item *Item) (*Item, []*Edge) { for _, li := range lis { edges = append(edges, &Edge{ - From: item.Reference(), - To: li.GetItem(), - BlastPropagation: li.GetBlastPropagation(), + From: item.Reference(), + To: li.GetItem(), }) } for _, liq := range liqs { edges = append(edges, &Edge{ - From: item.Reference(), - To: liq.GetQuery().Reference(), - BlastPropagation: liq.GetBlastPropagation(), + From: item.Reference(), + To: liq.GetQuery().Reference(), }) } return item, edges } +// Progress returns the current progress statistics for the query, including +// counts of responders in each state (Working, Stalled, Complete, Error, Cancelled). func (sq *SourceQuery) Progress() SourceQueryProgress { sq.respondersMu.Lock() defer sq.respondersMu.Unlock() @@ -706,6 +716,7 @@ func (sq *SourceQuery) Progress() SourceQueryProgress { } } +// String returns a human-readable summary of the query progress. func (sq *SourceQuery) String() string { progress := sq.Progress() diff --git a/sdp-go/progress_test.go b/go/sdp-go/progress_test.go similarity index 100% rename from sdp-go/progress_test.go rename to go/sdp-go/progress_test.go diff --git a/sdp-go/proto_clone_test.go b/go/sdp-go/proto_clone_test.go similarity index 88% rename from sdp-go/proto_clone_test.go rename to go/sdp-go/proto_clone_test.go index 0a73c4eb..863c8f7d 100644 --- a/sdp-go/proto_clone_test.go +++ b/go/sdp-go/proto_clone_test.go @@ -48,8 +48,7 @@ func TestProtoCloneReplacesCustomCopy(t *testing.T) { Scope: "scope", UUID: u[:], RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 5, - FollowOnlyBlastPropagation: true, + LinkDepth: 5, }, IgnoreCache: true, Deadline: timestamppb.Now(), @@ -97,17 +96,9 @@ func TestProtoCloneReplacesCustomCopy(t *testing.T) { }) t.Run("All other SDP types", func(t *testing.T) { - // BlastPropagation - bp := &BlastPropagation{In: true, Out: false} - bpClone := proto.Clone(bp).(*BlastPropagation) - if !proto.Equal(bp, bpClone) { - t.Errorf("proto.Clone failed for BlastPropagation") - } - // LinkedItemQuery liq := &LinkedItemQuery{ - Query: &Query{Type: "test", Method: QueryMethod_LIST}, - BlastPropagation: bp, + Query: &Query{Type: "test", Method: QueryMethod_LIST}, } liqClone := proto.Clone(liq).(*LinkedItemQuery) if !proto.Equal(liq, liqClone) { @@ -116,8 +107,7 @@ func TestProtoCloneReplacesCustomCopy(t *testing.T) { // LinkedItem li := &LinkedItem{ - Item: &Reference{Type: "test", Scope: "scope"}, - BlastPropagation: bp, + Item: &Reference{Type: "test", Scope: "scope"}, } liClone := proto.Clone(li).(*LinkedItem) if !proto.Equal(li, liClone) { diff --git a/sdp-go/responses.go b/go/sdp-go/responses.go similarity index 100% rename from sdp-go/responses.go rename to go/sdp-go/responses.go diff --git a/sdp-go/responses.pb.go b/go/sdp-go/responses.pb.go similarity index 98% rename from sdp-go/responses.pb.go rename to go/sdp-go/responses.pb.go index 677b024f..6d1628a7 100644 --- a/sdp-go/responses.pb.go +++ b/go/sdp-go/responses.pb.go @@ -189,7 +189,7 @@ const file_responses_proto_rawDesc = "" + "\bCOMPLETE\x10\x01\x12\t\n" + "\x05ERROR\x10\x02\x12\r\n" + "\tCANCELLED\x10\x03\x12\v\n" + - "\aSTALLED\x10\x04B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\aSTALLED\x10\x04B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_responses_proto_rawDescOnce sync.Once diff --git a/sdp-go/revlink.pb.go b/go/sdp-go/revlink.pb.go similarity index 94% rename from sdp-go/revlink.pb.go rename to go/sdp-go/revlink.pb.go index d152bdb2..6ede8fd4 100644 --- a/sdp-go/revlink.pb.go +++ b/go/sdp-go/revlink.pb.go @@ -27,11 +27,9 @@ type GetReverseEdgesRequest struct { // The account that the item belongs to Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` // The item that you would like to find reverse edges for - Item *Reference `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - // set to true to only return edges that propagate configuration change impact - FollowOnlyBlastPropagation bool `protobuf:"varint,3,opt,name=followOnlyBlastPropagation,proto3" json:"followOnlyBlastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Item *Reference `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetReverseEdgesRequest) Reset() { @@ -78,13 +76,6 @@ func (x *GetReverseEdgesRequest) GetItem() *Reference { return nil } -func (x *GetReverseEdgesRequest) GetFollowOnlyBlastPropagation() bool { - if x != nil { - return x.FollowOnlyBlastPropagation - } - return false -} - type GetReverseEdgesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The edges to the requested item @@ -351,12 +342,11 @@ var File_revlink_proto protoreflect.FileDescriptor const file_revlink_proto_rawDesc = "" + "\n" + - "\rrevlink.proto\x12\arevlink\x1a\vitems.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x92\x01\n" + + "\rrevlink.proto\x12\arevlink\x1a\vitems.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"t\n" + "\x16GetReverseEdgesRequest\x12\x18\n" + "\aaccount\x18\x01 \x01(\tR\aaccount\x12\x1e\n" + "\x04item\x18\x02 \x01(\v2\n" + - ".ReferenceR\x04item\x12>\n" + - "\x1afollowOnlyBlastPropagation\x18\x03 \x01(\bR\x1afollowOnlyBlastPropagation\"6\n" + + ".ReferenceR\x04itemJ\x04\b\x03\x10\x04R\x1afollowOnlyBlastPropagation\"6\n" + "\x17GetReverseEdgesResponse\x12\x1b\n" + "\x05edges\x18\x01 \x03(\v2\x05.EdgeR\x05edges\"\x8f\x01\n" + "\x1cIngestGatewayResponseRequest\x12\x18\n" + @@ -373,7 +363,7 @@ const file_revlink_proto_rawDesc = "" + "\x0fGetReverseEdges\x12\x1f.revlink.GetReverseEdgesRequest\x1a .revlink.GetReverseEdgesResponse\x12j\n" + "\x16IngestGatewayResponses\x12%.revlink.IngestGatewayResponseRequest\x1a'.revlink.IngestGatewayResponsesResponse(\x01\x12E\n" + "\n" + - "Checkpoint\x12\x1a.revlink.CheckpointRequest\x1a\x1b.revlink.CheckpointResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "Checkpoint\x12\x1a.revlink.CheckpointRequest\x1a\x1b.revlink.CheckpointResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_revlink_proto_rawDescOnce sync.Once diff --git a/sdp-go/sdpconnect/account.connect.go b/go/sdp-go/sdpconnect/account.connect.go similarity index 96% rename from sdp-go/sdpconnect/account.connect.go rename to go/sdp-go/sdpconnect/account.connect.go index 80989d25..1c9c8162 100644 --- a/sdp-go/sdpconnect/account.connect.go +++ b/go/sdp-go/sdpconnect/account.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) @@ -132,6 +132,9 @@ const ( // ManagementServiceUnsetGithubInstallationIDProcedure is the fully-qualified name of the // ManagementService's UnsetGithubInstallationID RPC. ManagementServiceUnsetGithubInstallationIDProcedure = "/account.ManagementService/UnsetGithubInstallationID" + // ManagementServiceGetOrCreateAWSExternalIdProcedure is the fully-qualified name of the + // ManagementService's GetOrCreateAWSExternalId RPC. + ManagementServiceGetOrCreateAWSExternalIdProcedure = "/account.ManagementService/GetOrCreateAWSExternalId" ) // AdminServiceClient is a client for the account.AdminService service. @@ -583,6 +586,9 @@ type ManagementServiceClient interface { // this will unset the github installation ID for the account, allowing the user to install the github app again // it will also remove the organisation profile, so we no longer generate signals for that org UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) + // Returns a stable, per-account external ID for AWS IAM trust policies. + // Generates a UUID on first call; returns the same UUID on subsequent calls. + GetOrCreateAWSExternalId(context.Context, *connect.Request[sdp_go.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp_go.GetOrCreateAWSExternalIdResponse], error) } // NewManagementServiceClient constructs a client for the account.ManagementService service. By @@ -722,6 +728,12 @@ func NewManagementServiceClient(httpClient connect.HTTPClient, baseURL string, o connect.WithSchema(managementServiceMethods.ByName("UnsetGithubInstallationID")), connect.WithClientOptions(opts...), ), + getOrCreateAWSExternalId: connect.NewClient[sdp_go.GetOrCreateAWSExternalIdRequest, sdp_go.GetOrCreateAWSExternalIdResponse]( + httpClient, + baseURL+ManagementServiceGetOrCreateAWSExternalIdProcedure, + connect.WithSchema(managementServiceMethods.ByName("GetOrCreateAWSExternalId")), + connect.WithClientOptions(opts...), + ), } } @@ -748,6 +760,7 @@ type managementServiceClient struct { getWelcomeScreenInformation *connect.Client[sdp_go.GetWelcomeScreenInformationRequest, sdp_go.GetWelcomeScreenInformationResponse] setGithubInstallationID *connect.Client[sdp_go.SetGithubInstallationIDRequest, sdp_go.SetGithubInstallationIDResponse] unsetGithubInstallationID *connect.Client[sdp_go.UnsetGithubInstallationIDRequest, sdp_go.UnsetGithubInstallationIDResponse] + getOrCreateAWSExternalId *connect.Client[sdp_go.GetOrCreateAWSExternalIdRequest, sdp_go.GetOrCreateAWSExternalIdResponse] } // GetAccount calls account.ManagementService.GetAccount. @@ -855,6 +868,11 @@ func (c *managementServiceClient) UnsetGithubInstallationID(ctx context.Context, return c.unsetGithubInstallationID.CallUnary(ctx, req) } +// GetOrCreateAWSExternalId calls account.ManagementService.GetOrCreateAWSExternalId. +func (c *managementServiceClient) GetOrCreateAWSExternalId(ctx context.Context, req *connect.Request[sdp_go.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp_go.GetOrCreateAWSExternalIdResponse], error) { + return c.getOrCreateAWSExternalId.CallUnary(ctx, req) +} + // ManagementServiceHandler is an implementation of the account.ManagementService service. type ManagementServiceHandler interface { // Get the details of the account that this user belongs to @@ -914,6 +932,9 @@ type ManagementServiceHandler interface { // this will unset the github installation ID for the account, allowing the user to install the github app again // it will also remove the organisation profile, so we no longer generate signals for that org UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) + // Returns a stable, per-account external ID for AWS IAM trust policies. + // Generates a UUID on first call; returns the same UUID on subsequent calls. + GetOrCreateAWSExternalId(context.Context, *connect.Request[sdp_go.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp_go.GetOrCreateAWSExternalIdResponse], error) } // NewManagementServiceHandler builds an HTTP handler from the service implementation. It returns @@ -1049,6 +1070,12 @@ func NewManagementServiceHandler(svc ManagementServiceHandler, opts ...connect.H connect.WithSchema(managementServiceMethods.ByName("UnsetGithubInstallationID")), connect.WithHandlerOptions(opts...), ) + managementServiceGetOrCreateAWSExternalIdHandler := connect.NewUnaryHandler( + ManagementServiceGetOrCreateAWSExternalIdProcedure, + svc.GetOrCreateAWSExternalId, + connect.WithSchema(managementServiceMethods.ByName("GetOrCreateAWSExternalId")), + connect.WithHandlerOptions(opts...), + ) return "/account.ManagementService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case ManagementServiceGetAccountProcedure: @@ -1093,6 +1120,8 @@ func NewManagementServiceHandler(svc ManagementServiceHandler, opts ...connect.H managementServiceSetGithubInstallationIDHandler.ServeHTTP(w, r) case ManagementServiceUnsetGithubInstallationIDProcedure: managementServiceUnsetGithubInstallationIDHandler.ServeHTTP(w, r) + case ManagementServiceGetOrCreateAWSExternalIdProcedure: + managementServiceGetOrCreateAWSExternalIdHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -1185,3 +1214,7 @@ func (UnimplementedManagementServiceHandler) SetGithubInstallationID(context.Con func (UnimplementedManagementServiceHandler) UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.UnsetGithubInstallationID is not implemented")) } + +func (UnimplementedManagementServiceHandler) GetOrCreateAWSExternalId(context.Context, *connect.Request[sdp_go.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp_go.GetOrCreateAWSExternalIdResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetOrCreateAWSExternalId is not implemented")) +} diff --git a/sdp-go/sdpconnect/apikeys.connect.go b/go/sdp-go/sdpconnect/apikeys.connect.go similarity index 99% rename from sdp-go/sdpconnect/apikeys.connect.go rename to go/sdp-go/sdpconnect/apikeys.connect.go index 6098fb80..62be0125 100644 --- a/sdp-go/sdpconnect/apikeys.connect.go +++ b/go/sdp-go/sdpconnect/apikeys.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/area51.connect.go b/go/sdp-go/sdpconnect/area51.connect.go similarity index 99% rename from sdp-go/sdpconnect/area51.connect.go rename to go/sdp-go/sdpconnect/area51.connect.go index 10098428..2cfec9cb 100644 --- a/sdp-go/sdpconnect/area51.connect.go +++ b/go/sdp-go/sdpconnect/area51.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/auth0support.connect.go b/go/sdp-go/sdpconnect/auth0support.connect.go similarity index 99% rename from sdp-go/sdpconnect/auth0support.connect.go rename to go/sdp-go/sdpconnect/auth0support.connect.go index f9b619d8..5435916e 100644 --- a/sdp-go/sdpconnect/auth0support.connect.go +++ b/go/sdp-go/sdpconnect/auth0support.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/bookmarks.connect.go b/go/sdp-go/sdpconnect/bookmarks.connect.go similarity index 99% rename from sdp-go/sdpconnect/bookmarks.connect.go rename to go/sdp-go/sdpconnect/bookmarks.connect.go index 796833de..579ff6ad 100644 --- a/sdp-go/sdpconnect/bookmarks.connect.go +++ b/go/sdp-go/sdpconnect/bookmarks.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/changes.connect.go b/go/sdp-go/sdpconnect/changes.connect.go similarity index 99% rename from sdp-go/sdpconnect/changes.connect.go rename to go/sdp-go/sdpconnect/changes.connect.go index 5b828fa8..2988499a 100644 --- a/sdp-go/sdpconnect/changes.connect.go +++ b/go/sdp-go/sdpconnect/changes.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/cli.connect.go b/go/sdp-go/sdpconnect/cli.connect.go similarity index 99% rename from sdp-go/sdpconnect/cli.connect.go rename to go/sdp-go/sdpconnect/cli.connect.go index f4910743..df4d52ff 100644 --- a/sdp-go/sdpconnect/cli.connect.go +++ b/go/sdp-go/sdpconnect/cli.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/config.connect.go b/go/sdp-go/sdpconnect/config.connect.go similarity index 91% rename from sdp-go/sdpconnect/config.connect.go rename to go/sdp-go/sdpconnect/config.connect.go index 9ae09c45..86befb73 100644 --- a/sdp-go/sdpconnect/config.connect.go +++ b/go/sdp-go/sdpconnect/config.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) @@ -48,6 +48,9 @@ const ( // ConfigurationServiceDeleteHcpConfigProcedure is the fully-qualified name of the // ConfigurationService's DeleteHcpConfig RPC. ConfigurationServiceDeleteHcpConfigProcedure = "/config.ConfigurationService/DeleteHcpConfig" + // ConfigurationServiceReplaceHcpApiKeyProcedure is the fully-qualified name of the + // ConfigurationService's ReplaceHcpApiKey RPC. + ConfigurationServiceReplaceHcpApiKeyProcedure = "/config.ConfigurationService/ReplaceHcpApiKey" // ConfigurationServiceGetSignalConfigProcedure is the fully-qualified name of the // ConfigurationService's GetSignalConfig RPC. ConfigurationServiceGetSignalConfigProcedure = "/config.ConfigurationService/GetSignalConfig" @@ -79,6 +82,10 @@ type ConfigurationServiceClient interface { GetHcpConfig(context.Context, *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) // Remove the existing HCP Terraform config from the user's account. DeleteHcpConfig(context.Context, *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) + // Replace the API key backing the HCP Terraform integration with a fresh + // one. The old API key is revoked. The endpoint URL and HMAC secret are + // preserved. Follows the same OAuth flow as CreateHcpConfig. + ReplaceHcpApiKey(context.Context, *connect.Request[sdp_go.ReplaceHcpApiKeyRequest]) (*connect.Response[sdp_go.ReplaceHcpApiKeyResponse], error) // Get the signal config for the account GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) // Update the signal config for the account @@ -134,6 +141,12 @@ func NewConfigurationServiceClient(httpClient connect.HTTPClient, baseURL string connect.WithSchema(configurationServiceMethods.ByName("DeleteHcpConfig")), connect.WithClientOptions(opts...), ), + replaceHcpApiKey: connect.NewClient[sdp_go.ReplaceHcpApiKeyRequest, sdp_go.ReplaceHcpApiKeyResponse]( + httpClient, + baseURL+ConfigurationServiceReplaceHcpApiKeyProcedure, + connect.WithSchema(configurationServiceMethods.ByName("ReplaceHcpApiKey")), + connect.WithClientOptions(opts...), + ), getSignalConfig: connect.NewClient[sdp_go.GetSignalConfigRequest, sdp_go.GetSignalConfigResponse]( httpClient, baseURL+ConfigurationServiceGetSignalConfigProcedure, @@ -174,6 +187,7 @@ type configurationServiceClient struct { createHcpConfig *connect.Client[sdp_go.CreateHcpConfigRequest, sdp_go.CreateHcpConfigResponse] getHcpConfig *connect.Client[sdp_go.GetHcpConfigRequest, sdp_go.GetHcpConfigResponse] deleteHcpConfig *connect.Client[sdp_go.DeleteHcpConfigRequest, sdp_go.DeleteHcpConfigResponse] + replaceHcpApiKey *connect.Client[sdp_go.ReplaceHcpApiKeyRequest, sdp_go.ReplaceHcpApiKeyResponse] getSignalConfig *connect.Client[sdp_go.GetSignalConfigRequest, sdp_go.GetSignalConfigResponse] updateSignalConfig *connect.Client[sdp_go.UpdateSignalConfigRequest, sdp_go.UpdateSignalConfigResponse] getGithubAppInformation *connect.Client[sdp_go.GetGithubAppInformationRequest, sdp_go.GetGithubAppInformationResponse] @@ -206,6 +220,11 @@ func (c *configurationServiceClient) DeleteHcpConfig(ctx context.Context, req *c return c.deleteHcpConfig.CallUnary(ctx, req) } +// ReplaceHcpApiKey calls config.ConfigurationService.ReplaceHcpApiKey. +func (c *configurationServiceClient) ReplaceHcpApiKey(ctx context.Context, req *connect.Request[sdp_go.ReplaceHcpApiKeyRequest]) (*connect.Response[sdp_go.ReplaceHcpApiKeyResponse], error) { + return c.replaceHcpApiKey.CallUnary(ctx, req) +} + // GetSignalConfig calls config.ConfigurationService.GetSignalConfig. func (c *configurationServiceClient) GetSignalConfig(ctx context.Context, req *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) { return c.getSignalConfig.CallUnary(ctx, req) @@ -246,6 +265,10 @@ type ConfigurationServiceHandler interface { GetHcpConfig(context.Context, *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) // Remove the existing HCP Terraform config from the user's account. DeleteHcpConfig(context.Context, *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) + // Replace the API key backing the HCP Terraform integration with a fresh + // one. The old API key is revoked. The endpoint URL and HMAC secret are + // preserved. Follows the same OAuth flow as CreateHcpConfig. + ReplaceHcpApiKey(context.Context, *connect.Request[sdp_go.ReplaceHcpApiKeyRequest]) (*connect.Response[sdp_go.ReplaceHcpApiKeyResponse], error) // Get the signal config for the account GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) // Update the signal config for the account @@ -297,6 +320,12 @@ func NewConfigurationServiceHandler(svc ConfigurationServiceHandler, opts ...con connect.WithSchema(configurationServiceMethods.ByName("DeleteHcpConfig")), connect.WithHandlerOptions(opts...), ) + configurationServiceReplaceHcpApiKeyHandler := connect.NewUnaryHandler( + ConfigurationServiceReplaceHcpApiKeyProcedure, + svc.ReplaceHcpApiKey, + connect.WithSchema(configurationServiceMethods.ByName("ReplaceHcpApiKey")), + connect.WithHandlerOptions(opts...), + ) configurationServiceGetSignalConfigHandler := connect.NewUnaryHandler( ConfigurationServiceGetSignalConfigProcedure, svc.GetSignalConfig, @@ -339,6 +368,8 @@ func NewConfigurationServiceHandler(svc ConfigurationServiceHandler, opts ...con configurationServiceGetHcpConfigHandler.ServeHTTP(w, r) case ConfigurationServiceDeleteHcpConfigProcedure: configurationServiceDeleteHcpConfigHandler.ServeHTTP(w, r) + case ConfigurationServiceReplaceHcpApiKeyProcedure: + configurationServiceReplaceHcpApiKeyHandler.ServeHTTP(w, r) case ConfigurationServiceGetSignalConfigProcedure: configurationServiceGetSignalConfigHandler.ServeHTTP(w, r) case ConfigurationServiceUpdateSignalConfigProcedure: @@ -378,6 +409,10 @@ func (UnimplementedConfigurationServiceHandler) DeleteHcpConfig(context.Context, return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.DeleteHcpConfig is not implemented")) } +func (UnimplementedConfigurationServiceHandler) ReplaceHcpApiKey(context.Context, *connect.Request[sdp_go.ReplaceHcpApiKeyRequest]) (*connect.Response[sdp_go.ReplaceHcpApiKeyResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.ReplaceHcpApiKey is not implemented")) +} + func (UnimplementedConfigurationServiceHandler) GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetSignalConfig is not implemented")) } diff --git a/sdp-go/sdpconnect/invites.connect.go b/go/sdp-go/sdpconnect/invites.connect.go similarity index 99% rename from sdp-go/sdpconnect/invites.connect.go rename to go/sdp-go/sdpconnect/invites.connect.go index 189a9d71..7830e156 100644 --- a/sdp-go/sdpconnect/invites.connect.go +++ b/go/sdp-go/sdpconnect/invites.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/logs.connect.go b/go/sdp-go/sdpconnect/logs.connect.go similarity index 99% rename from sdp-go/sdpconnect/logs.connect.go rename to go/sdp-go/sdpconnect/logs.connect.go index 16f403e5..daf1c535 100644 --- a/sdp-go/sdpconnect/logs.connect.go +++ b/go/sdp-go/sdpconnect/logs.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/revlink.connect.go b/go/sdp-go/sdpconnect/revlink.connect.go similarity index 99% rename from sdp-go/sdpconnect/revlink.connect.go rename to go/sdp-go/sdpconnect/revlink.connect.go index 673a48f9..7d706d9b 100644 --- a/sdp-go/sdpconnect/revlink.connect.go +++ b/go/sdp-go/sdpconnect/revlink.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/signal.connect.go b/go/sdp-go/sdpconnect/signal.connect.go similarity index 99% rename from sdp-go/sdpconnect/signal.connect.go rename to go/sdp-go/sdpconnect/signal.connect.go index 41df42f1..8f4ee7c5 100644 --- a/sdp-go/sdpconnect/signal.connect.go +++ b/go/sdp-go/sdpconnect/signal.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpconnect/snapshots.connect.go b/go/sdp-go/sdpconnect/snapshots.connect.go similarity index 99% rename from sdp-go/sdpconnect/snapshots.connect.go rename to go/sdp-go/sdpconnect/snapshots.connect.go index f787552c..35566688 100644 --- a/sdp-go/sdpconnect/snapshots.connect.go +++ b/go/sdp-go/sdpconnect/snapshots.connect.go @@ -8,7 +8,7 @@ import ( connect "connectrpc.com/connect" context "context" errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" + sdp_go "github.com/overmindtech/cli/go/sdp-go" http "net/http" strings "strings" ) diff --git a/sdp-go/sdpws/client.go b/go/sdp-go/sdpws/client.go similarity index 99% rename from sdp-go/sdpws/client.go rename to go/sdp-go/sdpws/client.go index 90d61608..7c7f62cf 100644 --- a/sdp-go/sdpws/client.go +++ b/go/sdp-go/sdpws/client.go @@ -11,8 +11,8 @@ import ( "github.com/coder/websocket" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "google.golang.org/protobuf/proto" ) diff --git a/sdp-go/sdpws/client_test.go b/go/sdp-go/sdpws/client_test.go similarity index 99% rename from sdp-go/sdpws/client_test.go rename to go/sdp-go/sdpws/client_test.go index f4bde01a..57fdd5dd 100644 --- a/sdp-go/sdpws/client_test.go +++ b/go/sdp-go/sdpws/client_test.go @@ -13,7 +13,7 @@ import ( "github.com/coder/websocket" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "go.uber.org/goleak" "google.golang.org/protobuf/proto" ) diff --git a/sdp-go/sdpws/messagehandler.go b/go/sdp-go/sdpws/messagehandler.go similarity index 99% rename from sdp-go/sdpws/messagehandler.go rename to go/sdp-go/sdpws/messagehandler.go index f4c52f61..e25fffdc 100644 --- a/sdp-go/sdpws/messagehandler.go +++ b/go/sdp-go/sdpws/messagehandler.go @@ -3,7 +3,7 @@ package sdpws import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" ) diff --git a/sdp-go/sdpws/utils.go b/go/sdp-go/sdpws/utils.go similarity index 99% rename from sdp-go/sdpws/utils.go rename to go/sdp-go/sdpws/utils.go index 6bdc737b..adc82a9b 100644 --- a/sdp-go/sdpws/utils.go +++ b/go/sdp-go/sdpws/utils.go @@ -7,7 +7,7 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/sdp-go/signal.pb.go b/go/sdp-go/signal.pb.go similarity index 99% rename from sdp-go/signal.pb.go rename to go/sdp-go/signal.pb.go index 4866cabd..aaa0e88b 100644 --- a/sdp-go/signal.pb.go +++ b/go/sdp-go/signal.pb.go @@ -1171,7 +1171,7 @@ const file_signal_proto_rawDesc = "" + "\x0eGetItemSignals\x12\x1d.signal.GetItemSignalsRequest\x1a\x1e.signal.GetItemSignalsResponse\x12U\n" + "\x10GetItemSignalsV2\x12\x1f.signal.GetItemSignalsRequestV2\x1a .signal.GetItemSignalsResponseV2\x12s\n" + "\x1aGetCustomSignalsByCategory\x12).signal.GetCustomSignalsByCategoryRequest\x1a*.signal.GetCustomSignalsByCategoryResponse\x12a\n" + - "\x14GetItemSignalDetails\x12#.signal.GetItemSignalDetailsRequest\x1a$.signal.GetItemSignalDetailsResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x14GetItemSignalDetails\x12#.signal.GetItemSignalDetailsRequest\x1a$.signal.GetItemSignalDetailsResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_signal_proto_rawDescOnce sync.Once diff --git a/sdp-go/signals.go b/go/sdp-go/signals.go similarity index 100% rename from sdp-go/signals.go rename to go/sdp-go/signals.go diff --git a/sdp-go/snapshots.go b/go/sdp-go/snapshots.go similarity index 73% rename from sdp-go/snapshots.go rename to go/sdp-go/snapshots.go index d6e45c71..8bbaf388 100644 --- a/sdp-go/snapshots.go +++ b/go/sdp-go/snapshots.go @@ -2,6 +2,7 @@ package sdp import "github.com/google/uuid" +// ToMap converts a Snapshot to a map for serialization. func (s *Snapshot) ToMap() map[string]any { return map[string]any{ "metadata": s.GetMetadata().ToMap(), @@ -9,6 +10,7 @@ func (s *Snapshot) ToMap() map[string]any { } } +// ToMap converts SnapshotMetadata to a map for serialization. func (sm *SnapshotMetadata) ToMap() map[string]any { return map[string]any{ "UUID": stringFromUuidBytes(sm.GetUUID()), @@ -16,6 +18,7 @@ func (sm *SnapshotMetadata) ToMap() map[string]any { } } +// GetUUIDParsed returns the parsed UUID from the SnapshotMetadata, or nil if invalid. func (sm *SnapshotMetadata) GetUUIDParsed() *uuid.UUID { if sm == nil { return nil @@ -27,6 +30,7 @@ func (sm *SnapshotMetadata) GetUUIDParsed() *uuid.UUID { return &u } +// ToMap converts SnapshotProperties to a map for serialization. func (sp *SnapshotProperties) ToMap() map[string]any { return map[string]any{ "name": sp.GetName(), diff --git a/sdp-go/snapshots.pb.go b/go/sdp-go/snapshots.pb.go similarity index 99% rename from sdp-go/snapshots.pb.go rename to go/sdp-go/snapshots.pb.go index edd6d6ec..01509b98 100644 --- a/sdp-go/snapshots.pb.go +++ b/go/sdp-go/snapshots.pb.go @@ -906,7 +906,7 @@ const file_snapshots_proto_rawDesc = "" + "\vGetSnapshot\x12\x1d.snapshots.GetSnapshotRequest\x1a\x1e.snapshots.GetSnapshotResponse\x12U\n" + "\x0eUpdateSnapshot\x12 .snapshots.UpdateSnapshotRequest\x1a!.snapshots.UpdateSnapshotResponse\x12U\n" + "\x0eDeleteSnapshot\x12 .snapshots.DeleteSnapshotRequest\x1a!.snapshots.DeleteSnapshotResponse\x12`\n" + - "\x11ListSnapshotByGUN\x12$.snapshots.ListSnapshotsByGUNRequest\x1a%.snapshots.ListSnapshotsByGUNResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x11ListSnapshotByGUN\x12$.snapshots.ListSnapshotsByGUNRequest\x1a%.snapshots.ListSnapshotsByGUNResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_snapshots_proto_rawDescOnce sync.Once diff --git a/sdp-go/test_utils.go b/go/sdp-go/test_utils.go similarity index 100% rename from sdp-go/test_utils.go rename to go/sdp-go/test_utils.go diff --git a/sdp-go/test_utils_test.go b/go/sdp-go/test_utils_test.go similarity index 100% rename from sdp-go/test_utils_test.go rename to go/sdp-go/test_utils_test.go diff --git a/sdp-go/tracing.go b/go/sdp-go/tracing.go similarity index 98% rename from sdp-go/tracing.go rename to go/sdp-go/tracing.go index d9ad2a79..16398f6c 100644 --- a/sdp-go/tracing.go +++ b/go/sdp-go/tracing.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/getsentry/sentry-go" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" ) diff --git a/sdp-go/tracing_test.go b/go/sdp-go/tracing_test.go similarity index 100% rename from sdp-go/tracing_test.go rename to go/sdp-go/tracing_test.go diff --git a/sdp-go/util.go b/go/sdp-go/util.go similarity index 100% rename from sdp-go/util.go rename to go/sdp-go/util.go diff --git a/sdp-go/util.pb.go b/go/sdp-go/util.pb.go similarity index 98% rename from sdp-go/util.pb.go rename to go/sdp-go/util.pb.go index 3e671d4e..bfdb6b5e 100644 --- a/sdp-go/util.pb.go +++ b/go/sdp-go/util.pb.go @@ -230,7 +230,7 @@ const file_util_proto_rawDesc = "" + "\x16ALPHABETICAL_ASCENDING\x10\x00\x12\x1b\n" + "\x17ALPHABETICAL_DESCENDING\x10\x01\x12\x12\n" + "\x0eDATE_ASCENDING\x10\x02\x12\x13\n" + - "\x0fDATE_DESCENDING\x10\x03B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" + "\x0fDATE_DESCENDING\x10\x03B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_util_proto_rawDescOnce sync.Once diff --git a/sdp-go/util_test.go b/go/sdp-go/util_test.go similarity index 100% rename from sdp-go/util_test.go rename to go/sdp-go/util_test.go diff --git a/sdp-go/validation.go b/go/sdp-go/validation.go similarity index 70% rename from sdp-go/validation.go rename to go/sdp-go/validation.go index 99596b96..5f084bae 100644 --- a/sdp-go/validation.go +++ b/go/sdp-go/validation.go @@ -5,7 +5,12 @@ import ( "fmt" ) -// Validate Ensures that en item is valid (e.g. contains the required fields) +// Validate ensures that an Item contains all required fields: +// - Type: must be non-empty +// - UniqueAttribute: must be non-empty +// - Attributes: must not be nil +// - Scope: must be non-empty +// - UniqueAttributeValue: must be non-empty (derived from Attributes) func (i *Item) Validate() error { if i == nil { return errors.New("Item is nil") @@ -34,7 +39,10 @@ func (i *Item) Validate() error { return nil } -// Validate Ensures a reference is valid +// Validate ensures a Reference contains all required fields: +// - Type: must be non-empty +// - UniqueAttributeValue: must be non-empty +// - Scope: must be non-empty func (r *Reference) Validate() error { if r == nil { return errors.New("reference is nil") @@ -52,7 +60,7 @@ func (r *Reference) Validate() error { return nil } -// Validate Ensures an edge is valid +// Validate ensures an Edge is valid by validating both the From and To references. func (e *Edge) Validate() error { if e == nil { return errors.New("edge is nil") @@ -70,7 +78,9 @@ func (e *Edge) Validate() error { return err } -// Validate Ensures a Response is valid +// Validate ensures a Response contains all required fields: +// - Responder: must be non-empty +// - UUID: must be non-empty func (r *Response) Validate() error { if r == nil { return errors.New("response is nil") @@ -87,7 +97,13 @@ func (r *Response) Validate() error { return nil } -// Validate Ensures a QueryError is valid +// Validate ensures a QueryError contains all required fields: +// - UUID: must be non-empty +// - ErrorString: must be non-empty +// - Scope: must be non-empty +// - SourceName: must be non-empty +// - ItemType: must be non-empty +// - ResponderName: must be non-empty func (e *QueryError) Validate() error { if e == nil { return errors.New("queryError is nil") @@ -120,7 +136,11 @@ func (e *QueryError) Validate() error { return nil } -// Validate Ensures a Query is valid +// Validate ensures a Query contains all required fields: +// - Type: must be non-empty +// - Scope: must be non-empty +// - UUID: must be exactly 16 bytes +// - Query: must be non-empty when method is GET func (q *Query) Validate() error { if q == nil { return errors.New("query is nil") diff --git a/sdp-go/validation_test.go b/go/sdp-go/validation_test.go similarity index 100% rename from sdp-go/validation_test.go rename to go/sdp-go/validation_test.go diff --git a/sdpcache/bolt_cache.go b/go/sdpcache/bolt_cache.go similarity index 99% rename from sdpcache/bolt_cache.go rename to go/sdpcache/bolt_cache.go index 5bd8933c..9d740310 100644 --- a/sdpcache/bolt_cache.go +++ b/go/sdpcache/bolt_cache.go @@ -13,8 +13,8 @@ import ( "time" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "go.etcd.io/bbolt" "go.opentelemetry.io/otel/attribute" diff --git a/sdpcache/cache.go b/go/sdpcache/cache.go similarity index 99% rename from sdpcache/cache.go rename to go/sdpcache/cache.go index a22aece0..d6089394 100644 --- a/sdpcache/cache.go +++ b/go/sdpcache/cache.go @@ -12,7 +12,7 @@ import ( "github.com/getsentry/sentry-go" "github.com/google/btree" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/sdpcache/cache_benchmark_test.go b/go/sdpcache/cache_benchmark_test.go similarity index 99% rename from sdpcache/cache_benchmark_test.go rename to go/sdpcache/cache_benchmark_test.go index e5aa2e19..893fda93 100644 --- a/sdpcache/cache_benchmark_test.go +++ b/go/sdpcache/cache_benchmark_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) const CacheDuration = 10 * time.Second diff --git a/sdpcache/cache_stuck_test.go b/go/sdpcache/cache_stuck_test.go similarity index 99% rename from sdpcache/cache_stuck_test.go rename to go/sdpcache/cache_stuck_test.go index c9c9713b..b5118e18 100644 --- a/sdpcache/cache_stuck_test.go +++ b/go/sdpcache/cache_stuck_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestListErrorWithProperCleanup tests the correct behavior where: diff --git a/sdpcache/cache_test.go b/go/sdpcache/cache_test.go similarity index 99% rename from sdpcache/cache_test.go rename to go/sdpcache/cache_test.go index d4e9c088..8003557f 100644 --- a/sdpcache/cache_test.go +++ b/go/sdpcache/cache_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // testSearch is a helper function that calls the internal search method diff --git a/sdpcache/item_generator_test.go b/go/sdpcache/item_generator_test.go similarity index 96% rename from sdpcache/item_generator_test.go rename to go/sdpcache/item_generator_test.go index ea21ff0d..602a5c8b 100644 --- a/sdpcache/item_generator_test.go +++ b/go/sdpcache/item_generator_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -82,8 +82,7 @@ func GenerateRandomItem() *sdp.Item { Method: sdp.QueryMethod(rand.Intn(3)), Query: randSeq(rand.Intn(MaxAttributeValueLength)), RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: rand.Uint32(), - FollowOnlyBlastPropagation: rand.Intn(2) == 0, + LinkDepth: rand.Uint32(), }, Scope: randSeq(rand.Intn(MaxAttributeKeyLength)), }} diff --git a/sdpcache/pending.go b/go/sdpcache/pending.go similarity index 100% rename from sdpcache/pending.go rename to go/sdpcache/pending.go diff --git a/tracing/deferlog.go b/go/tracing/deferlog.go similarity index 100% rename from tracing/deferlog.go rename to go/tracing/deferlog.go diff --git a/tracing/header_carrier.go b/go/tracing/header_carrier.go similarity index 100% rename from tracing/header_carrier.go rename to go/tracing/header_carrier.go diff --git a/tracing/main.go b/go/tracing/main.go similarity index 99% rename from tracing/main.go rename to go/tracing/main.go index b379c972..013b65e3 100644 --- a/tracing/main.go +++ b/go/tracing/main.go @@ -33,7 +33,7 @@ const instrumentationName = "github.com/overmindtech/workspace" // the following vars will be set during the build using `ldflags`, eg: // -// go build -ldflags "-X github.com/overmindtech/cli/tracing.version=$VERSION" -o your-app +// go build -ldflags "-X github.com/overmindtech/cli/go/tracing.version=$VERSION" -o your-app // // This allows caching to work for dev and removes the last `go generate` // requirement from the build. If we were embedding the version here each time diff --git a/tracing/main_test.go b/go/tracing/main_test.go similarity index 100% rename from tracing/main_test.go rename to go/tracing/main_test.go diff --git a/tracing/memory.go b/go/tracing/memory.go similarity index 100% rename from tracing/memory.go rename to go/tracing/memory.go diff --git a/tracing/memory_test.go b/go/tracing/memory_test.go similarity index 100% rename from tracing/memory_test.go rename to go/tracing/memory_test.go diff --git a/k8s-source/adapters/clusterrole.go b/k8s-source/adapters/clusterrole.go index 955436d4..c957f2e6 100644 --- a/k8s-source/adapters/clusterrole.go +++ b/k8s-source/adapters/clusterrole.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/rbac/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/clusterrole_test.go b/k8s-source/adapters/clusterrole_test.go index efb06ad2..9651f356 100644 --- a/k8s-source/adapters/clusterrole_test.go +++ b/k8s-source/adapters/clusterrole_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var clusterRoleYAML = ` diff --git a/k8s-source/adapters/clusterrolebinding.go b/k8s-source/adapters/clusterrolebinding.go index 17d479f0..27d09861 100644 --- a/k8s-source/adapters/clusterrolebinding.go +++ b/k8s-source/adapters/clusterrolebinding.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/rbac/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -19,13 +19,6 @@ func clusterRoleBindingExtractor(resource *v1.ClusterRoleBinding, scope string) Query: resource.RoleRef.Name, Type: resource.RoleRef.Kind, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the role will affect the things bound to it since the - // role contains the permissions - In: true, - // Changes to the binding won't affect the role - Out: false, - }, }) for _, subject := range resource.Subjects { @@ -44,12 +37,6 @@ func clusterRoleBindingExtractor(resource *v1.ClusterRoleBinding, scope string) Query: subject.Name, Type: subject.Kind, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the group won't affect the binding itself - In: false, - // Changes to the binding will affect the group it's bound to - Out: true, - }, }) } diff --git a/k8s-source/adapters/clusterrolebinding_test.go b/k8s-source/adapters/clusterrolebinding_test.go index db5adaed..e2b2c4aa 100644 --- a/k8s-source/adapters/clusterrolebinding_test.go +++ b/k8s-source/adapters/clusterrolebinding_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var clusterRoleBindingYAML = ` diff --git a/k8s-source/adapters/configmap.go b/k8s-source/adapters/configmap.go index a25c99cd..1cb7928e 100644 --- a/k8s-source/adapters/configmap.go +++ b/k8s-source/adapters/configmap.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/configmap_test.go b/k8s-source/adapters/configmap_test.go index 51d7b117..a5c256cc 100644 --- a/k8s-source/adapters/configmap_test.go +++ b/k8s-source/adapters/configmap_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var configMapYAML = ` diff --git a/k8s-source/adapters/cronjob.go b/k8s-source/adapters/cronjob.go index 7180f115..21c9bea3 100644 --- a/k8s-source/adapters/cronjob.go +++ b/k8s-source/adapters/cronjob.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/batch/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/cronjob_test.go b/k8s-source/adapters/cronjob_test.go index e8c6f476..fe86adb6 100644 --- a/k8s-source/adapters/cronjob_test.go +++ b/k8s-source/adapters/cronjob_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var cronJobYAML = ` @@ -50,8 +50,10 @@ func TestCronJobAdapter(t *testing.T) { // created it jobAdapter := newJobAdapter(CurrentCluster.ClientSet, sd.ClusterName, []string{sd.Namespace}, sdpcache.NewNoOpCache()) - // Wait for the job to be created - err := WaitFor(60*time.Second, func() bool { + // Wait for the CronJob controller to spawn a Job. The schedule is + // "* * * * *" (once per minute), so in the worst case we wait just over + // 60 seconds. 120 s gives comfortable headroom and avoids flakes. + err := WaitFor(120*time.Second, func() bool { jobs, err := jobAdapter.List(context.Background(), sd.String(), false) if err != nil { diff --git a/k8s-source/adapters/daemonset.go b/k8s-source/adapters/daemonset.go index 091b62bd..3ed80a30 100644 --- a/k8s-source/adapters/daemonset.go +++ b/k8s-source/adapters/daemonset.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/apps/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/daemonset_test.go b/k8s-source/adapters/daemonset_test.go index bedea3fa..d4e4ef61 100644 --- a/k8s-source/adapters/daemonset_test.go +++ b/k8s-source/adapters/daemonset_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var daemonSetYAML = ` diff --git a/k8s-source/adapters/deployment.go b/k8s-source/adapters/deployment.go index 12916a45..b4d64368 100644 --- a/k8s-source/adapters/deployment.go +++ b/k8s-source/adapters/deployment.go @@ -3,9 +3,9 @@ package adapters import ( "regexp" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/apps/v1" "k8s.io/client-go/kubernetes" @@ -55,11 +55,6 @@ func newDeploymentAdapter(cs *kubernetes.Clientset, cluster string, namespaces [ Query: matches[1], Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly bound - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/deployment_test.go b/k8s-source/adapters/deployment_test.go index 53219651..f435a9bd 100644 --- a/k8s-source/adapters/deployment_test.go +++ b/k8s-source/adapters/deployment_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var deploymentYAML = ` diff --git a/k8s-source/adapters/endpoints.go b/k8s-source/adapters/endpoints.go index 556a9b74..ef90ec11 100644 --- a/k8s-source/adapters/endpoints.go +++ b/k8s-source/adapters/endpoints.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -27,11 +27,6 @@ func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItem Query: address.Hostname, Type: "dns", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over DNS - In: true, - Out: true, - }, }) } @@ -43,12 +38,6 @@ func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItem Method: sdp.QueryMethod_GET, Query: *address.NodeName, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the node can affect the endpoint - In: true, - // Changes to the endpoint cannot affect the node - Out: false, - }, }) } @@ -60,20 +49,11 @@ func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItem Query: address.IP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over IP - In: true, - Out: true, - }, }) } if address.TargetRef != nil { - queries = append(queries, ObjectReferenceToQuery(address.TargetRef, sd, &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - })) + queries = append(queries, ObjectReferenceToQuery(address.TargetRef, sd)) } } } diff --git a/k8s-source/adapters/endpoints_test.go b/k8s-source/adapters/endpoints_test.go index cff9109a..732e1b9f 100644 --- a/k8s-source/adapters/endpoints_test.go +++ b/k8s-source/adapters/endpoints_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var endpointsYAML = ` diff --git a/k8s-source/adapters/endpointslice.go b/k8s-source/adapters/endpointslice.go index de68e992..b2fc027e 100644 --- a/k8s-source/adapters/endpointslice.go +++ b/k8s-source/adapters/endpointslice.go @@ -5,9 +5,9 @@ import ( v1 "k8s.io/api/discovery/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -29,11 +29,6 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li Query: *endpoint.Hostname, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over DNS - In: true, - Out: true, - }, }) } @@ -45,21 +40,11 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li Query: *endpoint.NodeName, Scope: sd.ClusterName, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the node can affect the endpoint - In: true, - // Changes to the endpoint cannot affect the node - Out: false, - }, }) } if endpoint.TargetRef != nil { - queries = append(queries, ObjectReferenceToQuery(endpoint.TargetRef, sd, &sdp.BlastPropagation{ - // Changes to the pod could affect the endpoint and vice versa - In: true, - Out: true, - })) + queries = append(queries, ObjectReferenceToQuery(endpoint.TargetRef, sd)) } for _, address := range endpoint.Addresses { @@ -72,11 +57,6 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li Query: address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over IP - In: true, - Out: true, - }, }) case v1.AddressTypeFQDN: queries = append(queries, &sdp.LinkedItemQuery{ @@ -86,11 +66,6 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li Query: address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over DNS - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/endpointslice_test.go b/k8s-source/adapters/endpointslice_test.go index 1d41389f..e1d99776 100644 --- a/k8s-source/adapters/endpointslice_test.go +++ b/k8s-source/adapters/endpointslice_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var endpointSliceYAML = ` diff --git a/k8s-source/adapters/generic_source.go b/k8s-source/adapters/generic_source.go index 5ca362a8..6f331f59 100644 --- a/k8s-source/adapters/generic_source.go +++ b/k8s-source/adapters/generic_source.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" corev1 "k8s.io/api/core/v1" k8serr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -393,17 +393,6 @@ func (s *KubeTypeAdapter[Resource, ResourceList]) resourceToItem(resource Resour Query: ref.Name, Scope: sd.String(), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the owner will definitely affect the owned e.g. - // changes to a deployment will affect the pods in that - // deployment - In: true, - // Changes to the owned may affect the owner e.g. changing a - // secret could affect a pod, but if all pods used that secret - // then the change should propagate from the pods to the - // deployment too - Out: true, - }, }) } @@ -434,7 +423,7 @@ func (s *KubeTypeAdapter[Resource, ResourceList]) resourceToItem(resource Resour // request. Note that you must provide the parent scope since the reference // could be an object in a different namespace, if it is we need to re-use the // cluster name from the parent scope -func ObjectReferenceToQuery(ref *corev1.ObjectReference, parentScope ScopeDetails, blastProp *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ObjectReferenceToQuery(ref *corev1.ObjectReference, parentScope ScopeDetails) *sdp.LinkedItemQuery { if ref == nil { return nil } @@ -449,7 +438,6 @@ func ObjectReferenceToQuery(ref *corev1.ObjectReference, parentScope ScopeDetail Query: ref.Name, Scope: parentScope.String(), }, - BlastPropagation: blastProp, } } diff --git a/k8s-source/adapters/generic_source_test.go b/k8s-source/adapters/generic_source_test.go index 7fcf5eb8..2635e80a 100644 --- a/k8s-source/adapters/generic_source_test.go +++ b/k8s-source/adapters/generic_source_test.go @@ -9,9 +9,9 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -727,12 +727,10 @@ func TestObjectReferenceToQuery(t *testing.T) { Name: "foo", } - b := &sdp.BlastPropagation{} - query := ObjectReferenceToQuery(ref, ScopeDetails{ ClusterName: "test-cluster", Namespace: "default", - }, b) + }) if query.GetQuery().GetType() != "Pod" { t.Errorf("expected type Pod, got %s", query.GetQuery().GetType()) @@ -745,14 +743,10 @@ func TestObjectReferenceToQuery(t *testing.T) { if query.GetQuery().GetScope() != "test-cluster.default" { t.Errorf("expected scope to be test-cluster.default, got %s", query.GetQuery().GetScope()) } - - if query.GetBlastPropagation() != b { - t.Errorf("expected blast propagation to be %v, got %v", b, query.GetBlastPropagation()) - } }) t.Run("with a nil object reference", func(t *testing.T) { - query := ObjectReferenceToQuery(nil, ScopeDetails{}, nil) + query := ObjectReferenceToQuery(nil, ScopeDetails{}) if query != nil { t.Errorf("expected nil query, got %v", query) diff --git a/k8s-source/adapters/horizontalpodautoscaler.go b/k8s-source/adapters/horizontalpodautoscaler.go index db7d74ff..b2c63125 100644 --- a/k8s-source/adapters/horizontalpodautoscaler.go +++ b/k8s-source/adapters/horizontalpodautoscaler.go @@ -3,9 +3,9 @@ package adapters import ( v2 "k8s.io/api/autoscaling/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -19,12 +19,6 @@ func horizontalPodAutoscalerExtractor(resource *v2.HorizontalPodAutoscaler, scop Query: resource.Spec.ScaleTargetRef.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the target won't affect the hpa - In: false, - // Changes to the hpa can affect the target i.e. by scaling the pods - Out: true, - }, }) return queries, nil diff --git a/k8s-source/adapters/horizontalpodautoscaler_test.go b/k8s-source/adapters/horizontalpodautoscaler_test.go index 7d5cf303..e5f87785 100644 --- a/k8s-source/adapters/horizontalpodautoscaler_test.go +++ b/k8s-source/adapters/horizontalpodautoscaler_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var horizontalPodAutoscalerYAML = ` diff --git a/k8s-source/adapters/ingress.go b/k8s-source/adapters/ingress.go index 45fab9f0..d400ecb5 100644 --- a/k8s-source/adapters/ingress.go +++ b/k8s-source/adapters/ingress.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/networking/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -20,13 +20,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: *resource.Spec.IngressClassName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the ingress (e.g. nginx) class can affect the - // ingresses that use it - In: true, - // Changes to an ingress wont' affect the class - Out: false, - }, }) } @@ -39,12 +32,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: resource.Spec.DefaultBackend.Service.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the service affects the ingress' endpoints - In: true, - // Changing an ingress does not affect the service - Out: false, - }, }) } @@ -56,13 +43,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: linkRes.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the default backend won't affect the ingress - // itself - In: false, - // Changes to the ingress could affect the default backend - Out: true, - }, }) } } @@ -76,11 +56,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: rule.Host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate through rules - In: true, - Out: true, - }, }) } @@ -94,12 +69,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: path.Backend.Service.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the service affects the ingress' endpoints - In: true, - // Changing an ingress does not affect the service - Out: false, - }, }) } @@ -111,14 +80,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: path.Backend.Resource.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes can go in both directions here. An - // backend change can affect the ingress by rendering - // backend change can affect the ingress by rending - // it broken - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/ingress_test.go b/k8s-source/adapters/ingress_test.go index 16a8ef1a..f3c8990c 100644 --- a/k8s-source/adapters/ingress_test.go +++ b/k8s-source/adapters/ingress_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var ingressYAML = ` diff --git a/k8s-source/adapters/job.go b/k8s-source/adapters/job.go index fce17eec..fa81d0d4 100644 --- a/k8s-source/adapters/job.go +++ b/k8s-source/adapters/job.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/batch/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -20,12 +20,6 @@ func jobExtractor(resource *v1.Job, scope string) ([]*sdp.LinkedItemQuery, error Query: LabelSelectorToQuery(resource.Spec.Selector), Type: "Pod", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a job will replace the pods, changes to the pods - // could break the job - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/job_test.go b/k8s-source/adapters/job_test.go index fa69c4ef..8cc9cb0a 100644 --- a/k8s-source/adapters/job_test.go +++ b/k8s-source/adapters/job_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var jobYAML = ` diff --git a/k8s-source/adapters/limitrange.go b/k8s-source/adapters/limitrange.go index a0c19298..bf402bde 100644 --- a/k8s-source/adapters/limitrange.go +++ b/k8s-source/adapters/limitrange.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/limitrange_test.go b/k8s-source/adapters/limitrange_test.go index c28b5fa0..b31120b4 100644 --- a/k8s-source/adapters/limitrange_test.go +++ b/k8s-source/adapters/limitrange_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var limitRangeYAML = ` diff --git a/k8s-source/adapters/main.go b/k8s-source/adapters/main.go index 3fcb4051..a9987f5b 100644 --- a/k8s-source/adapters/main.go +++ b/k8s-source/adapters/main.go @@ -1,8 +1,8 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/networkpolicy.go b/k8s-source/adapters/networkpolicy.go index fa6b9810..5af261a8 100644 --- a/k8s-source/adapters/networkpolicy.go +++ b/k8s-source/adapters/networkpolicy.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/networking/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -19,13 +19,6 @@ func NetworkPolicyExtractor(resource *v1.NetworkPolicy, scope string) ([]*sdp.Li Query: LabelSelectorToQuery(&resource.Spec.PodSelector), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to pods won't affect the network policy or anything else - // that shares it - In: false, - // Changes to the network policy will affect pods - Out: true, - }, }) var peers []v1.NetworkPolicyPeer @@ -53,13 +46,6 @@ func NetworkPolicyExtractor(resource *v1.NetworkPolicy, scope string) ([]*sdp.Li Query: LabelSelectorToQuery(ps), Type: "Pod", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to pods won't affect the network policy or anything else - // that shares it - In: false, - // Changes to the network policy will affect pods - Out: true, - }, }) } } diff --git a/k8s-source/adapters/networkpolicy_test.go b/k8s-source/adapters/networkpolicy_test.go index b489a0e8..a24ef700 100644 --- a/k8s-source/adapters/networkpolicy_test.go +++ b/k8s-source/adapters/networkpolicy_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var NetworkPolicyYAML = ` diff --git a/k8s-source/adapters/node.go b/k8s-source/adapters/node.go index 4c066308..6156c207 100644 --- a/k8s-source/adapters/node.go +++ b/k8s-source/adapters/node.go @@ -3,9 +3,9 @@ package adapters import ( "strings" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" @@ -24,11 +24,6 @@ func linkedItemExtractor(resource *v1.Node, scope string) ([]*sdp.LinkedItemQuer Query: addr.Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over DNS - In: true, - Out: true, - }, }) case v1.NodeExternalIP, v1.NodeInternalIP: @@ -39,11 +34,6 @@ func linkedItemExtractor(resource *v1.Node, scope string) ([]*sdp.LinkedItemQuer Query: addr.Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over IP - In: true, - Out: true, - }, }) } } @@ -62,12 +52,6 @@ func linkedItemExtractor(resource *v1.Node, scope string) ([]*sdp.LinkedItemQuer Query: sections[1], Scope: "*", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the volume can affect the node - In: true, - // Changes to the node cannot affect the volume - Out: true, - }, }) } } diff --git a/k8s-source/adapters/node_test.go b/k8s-source/adapters/node_test.go index a268cfd4..ffd49f7a 100644 --- a/k8s-source/adapters/node_test.go +++ b/k8s-source/adapters/node_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNodeAdapter(t *testing.T) { diff --git a/k8s-source/adapters/persistentvolume.go b/k8s-source/adapters/persistentvolume.go index 6492dc05..c94a1c68 100644 --- a/k8s-source/adapters/persistentvolume.go +++ b/k8s-source/adapters/persistentvolume.go @@ -3,9 +3,9 @@ package adapters import ( "regexp" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -28,12 +28,6 @@ func PersistentVolumeExtractor(resource *v1.PersistentVolume, scope string) ([]* Query: resource.Spec.PersistentVolumeSource.AWSElasticBlockStore.VolumeID, Scope: "*", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the EBS volume can affect the PV - In: true, - // Changes to the PV might affect the EBS volume - Out: true, - }, }) } @@ -52,24 +46,13 @@ func PersistentVolumeExtractor(resource *v1.PersistentVolume, scope string) ([]* Query: matches[1], Scope: "*", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the EFS access point can affect the PV - In: true, - // Changes to the PV won't affect the EFS access point - Out: false, - }, }) } } } if resource.Spec.ClaimRef != nil { - queries = append(queries, ObjectReferenceToQuery(resource.Spec.ClaimRef, sd, &sdp.BlastPropagation{ - // Changing claim might not affect the PV - In: false, - // Changing the PV will definitely affect the claim - Out: true, - })) + queries = append(queries, ObjectReferenceToQuery(resource.Spec.ClaimRef, sd)) } if resource.Spec.StorageClassName != "" { @@ -80,12 +63,6 @@ func PersistentVolumeExtractor(resource *v1.PersistentVolume, scope string) ([]* Query: resource.Spec.StorageClassName, Scope: sd.ClusterName, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the storage class can affect the PV - In: true, - // Changes to the PV cannot affect the storage class - Out: false, - }, }) } diff --git a/k8s-source/adapters/persistentvolume_test.go b/k8s-source/adapters/persistentvolume_test.go index cc15002a..d154860b 100644 --- a/k8s-source/adapters/persistentvolume_test.go +++ b/k8s-source/adapters/persistentvolume_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var persistentVolumeYAML = ` diff --git a/k8s-source/adapters/persistentvolumeclaim.go b/k8s-source/adapters/persistentvolumeclaim.go index 70e66384..c739c837 100644 --- a/k8s-source/adapters/persistentvolumeclaim.go +++ b/k8s-source/adapters/persistentvolumeclaim.go @@ -3,9 +3,9 @@ package adapters import ( "errors" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -25,13 +25,6 @@ func PersistentVolumeClaimExtractor(resource *v1.PersistentVolumeClaim, scope st Query: resource.Spec.VolumeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the volume could affect the claim - In: true, - // Changes to the claim could affect the volume if there are - // other claims - Out: true, - }, }) } diff --git a/k8s-source/adapters/persistentvolumeclaim_test.go b/k8s-source/adapters/persistentvolumeclaim_test.go index a5d0e47b..698b1ebb 100644 --- a/k8s-source/adapters/persistentvolumeclaim_test.go +++ b/k8s-source/adapters/persistentvolumeclaim_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var persistentVolumeClaimYAML = ` diff --git a/k8s-source/adapters/poddisruptionbudget.go b/k8s-source/adapters/poddisruptionbudget.go index 12f0d58c..2a43e902 100644 --- a/k8s-source/adapters/poddisruptionbudget.go +++ b/k8s-source/adapters/poddisruptionbudget.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/policy/v1" "k8s.io/client-go/kubernetes" ) @@ -19,12 +19,6 @@ func podDisruptionBudgetExtractor(resource *v1.PodDisruptionBudget, scope string Query: LabelSelectorToQuery(resource.Spec.Selector), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to pods won't affect the disruption budget - In: false, - // Changes to the disruption budget will affect pods - Out: true, - }, }) } diff --git a/k8s-source/adapters/poddisruptionbudget_test.go b/k8s-source/adapters/poddisruptionbudget_test.go index f5cba81c..93e2bc38 100644 --- a/k8s-source/adapters/poddisruptionbudget_test.go +++ b/k8s-source/adapters/poddisruptionbudget_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var PodDisruptionBudgetYAML = ` diff --git a/k8s-source/adapters/pods.go b/k8s-source/adapters/pods.go index d5e61013..3e9a6b01 100644 --- a/k8s-source/adapters/pods.go +++ b/k8s-source/adapters/pods.go @@ -5,9 +5,9 @@ import ( "slices" "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -30,12 +30,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Method: sdp.QueryMethod_GET, Query: resource.Spec.ServiceAccountName, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the service account can affect the pod - In: true, - // Changes to the pod cannot affect the service account - Out: false, - }, }) } @@ -50,13 +44,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.PersistentVolumeClaim.ClaimName, Type: "PersistentVolumeClaim", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the PVC will affect the pod - In: true, - // The pod can definitely affect the PVC, by filling it up - // for example - Out: true, - }, }) } @@ -69,12 +56,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.AWSElasticBlockStore.VolumeID, Type: "ec2-volume", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the volume will affect the pod - In: true, - // The pod can definitely affect the volume - Out: true, - }, }) } @@ -87,12 +68,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.Secret.SecretName, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret could easily break the pod - In: true, - // The pod however isn't going to affect a secret - Out: false, - }, }) } @@ -108,12 +83,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.NFS.Server, Type: "ip", }, - BlastPropagation: &sdp.BlastPropagation{ - // NFS server can affect the pod - In: true, - // Pod can't affect the NFS server - Out: false, - }, }) } else { queries = append(queries, &sdp.LinkedItemQuery{ @@ -123,12 +92,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Type: "dns", Query: vol.NFS.Server, }, - BlastPropagation: &sdp.BlastPropagation{ - // NFS server can affect the pod - In: true, - // Pod can't affect the NFS server - Out: false, - }, }) } } @@ -142,12 +105,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.ConfigMap.Name, Type: "ConfigMap", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config map could easily break the pod - In: true, - // The pod however isn't going to affect a config map - Out: false, - }, }) } @@ -162,12 +119,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: source.ConfigMap.Name, Type: "ConfigMap", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config map could easily break the pod - In: true, - // The pod however isn't going to affect a config map - Out: false, - }, }) } @@ -179,12 +130,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: source.Secret.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret could easily break the pod - In: true, - // The pod however isn't going to affect a secret - Out: false, - }, }) } } @@ -205,12 +150,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: env.ValueFrom.SecretKeyRef.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret could easily break the pod - In: true, - // The pod however isn't going to affect a secret - Out: false, - }, }) } @@ -222,12 +161,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: env.ValueFrom.ConfigMapKeyRef.Name, Type: "ConfigMap", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config map could easily break the pod - In: true, - // The pod however isn't going to affect a config map - Out: false, - }, }) } } @@ -243,12 +176,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: envFrom.SecretRef.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret could easily break the pod - In: true, - // The pod however isn't going to affect a secret - Out: false, - }, }) } @@ -260,12 +187,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: envFrom.ConfigMapRef.Name, Type: "ConfigMap", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config map could easily break the pod - In: true, - // The pod however isn't going to affect a config map - Out: false, - }, }) } } @@ -279,14 +200,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: resource.Spec.PriorityClassName, Type: "PriorityClass", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the priority class could break a pod by meaning that - // it would now be scheduled with a lower priority and could - // therefore end up pending for ages - In: true, - // The pod however isn't going to affect a priority class - Out: false, - }, }) } @@ -299,11 +212,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: ip.IP, Type: "ip", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs go in both directions - In: true, - Out: true, - }, }) } } else if resource.Status.PodIP != "" { @@ -314,11 +222,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: resource.Status.PodIP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs go in both directions - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/pods_test.go b/k8s-source/adapters/pods_test.go index fbe6f282..d99e6b81 100644 --- a/k8s-source/adapters/pods_test.go +++ b/k8s-source/adapters/pods_test.go @@ -5,9 +5,10 @@ import ( "fmt" "regexp" "testing" + "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" ) @@ -183,23 +184,33 @@ func TestPodAdapter(t *testing.T) { } st.Execute(t) - // the pods are still running let check their health - // get the bad pod - item, err := adapter.Get(context.Background(), sd.String(), "pod-bad-pod", true) + // Wait for the bad pod's image pull to fail before asserting health. + // The kubelet needs time to attempt the pull and enter + // ErrImagePull / ImagePullBackOff, which surfaces as HEALTH_ERROR. + var badPodItem *sdp.Item + err := WaitFor(60*time.Second, func() bool { + item, err := adapter.Get(context.Background(), sd.String(), "pod-bad-pod", true) + if err != nil { + return false + } + badPodItem = item + return item.GetHealth() == sdp.Health_HEALTH_ERROR + }) if err != nil { - t.Fatal(fmt.Errorf("failed to get pod: %w", err)) - } - if item.GetHealth() != sdp.Health_HEALTH_ERROR { - t.Errorf("expected status to be unhealthy, got %s", item.GetHealth()) + health := sdp.Health_HEALTH_UNKNOWN + if badPodItem != nil { + health = badPodItem.GetHealth() + } + t.Fatalf("expected bad pod health to reach HEALTH_ERROR, still %s after timeout", health) } // get the healthy pod - item, err = adapter.Get(context.Background(), sd.String(), "pod-test-pod", true) + healthyItem, err := adapter.Get(context.Background(), sd.String(), "pod-test-pod", true) if err != nil { t.Fatal(fmt.Errorf("failed to get pod: %w", err)) } - if item.GetHealth() != sdp.Health_HEALTH_OK { - t.Errorf("expected status to be healthy, got %s", item.GetHealth()) + if healthyItem.GetHealth() != sdp.Health_HEALTH_OK { + t.Errorf("expected status to be healthy, got %s", healthyItem.GetHealth()) } } diff --git a/k8s-source/adapters/priorityclass.go b/k8s-source/adapters/priorityclass.go index 613c909d..f3274ac1 100644 --- a/k8s-source/adapters/priorityclass.go +++ b/k8s-source/adapters/priorityclass.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/scheduling/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/priorityclass_test.go b/k8s-source/adapters/priorityclass_test.go index 6e70d913..adf96207 100644 --- a/k8s-source/adapters/priorityclass_test.go +++ b/k8s-source/adapters/priorityclass_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var priorityClassYAML = ` diff --git a/k8s-source/adapters/replicaset.go b/k8s-source/adapters/replicaset.go index e466fe0f..b38554c2 100644 --- a/k8s-source/adapters/replicaset.go +++ b/k8s-source/adapters/replicaset.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/apps/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -20,12 +20,6 @@ func replicaSetExtractor(resource *v1.ReplicaSet, scope string) ([]*sdp.LinkedIt Query: LabelSelectorToQuery(resource.Spec.Selector), Type: "Pod", }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the service - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/replicaset_test.go b/k8s-source/adapters/replicaset_test.go index 37e0a3df..69adb2fa 100644 --- a/k8s-source/adapters/replicaset_test.go +++ b/k8s-source/adapters/replicaset_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var replicaSetYAML = ` diff --git a/k8s-source/adapters/replicationcontroller.go b/k8s-source/adapters/replicationcontroller.go index f4f8bf19..e4d6531b 100644 --- a/k8s-source/adapters/replicationcontroller.go +++ b/k8s-source/adapters/replicationcontroller.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -22,12 +22,6 @@ func replicationControllerExtractor(resource *v1.ReplicationController, scope st }), Type: "Pod", }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the service - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/replicationcontroller_test.go b/k8s-source/adapters/replicationcontroller_test.go index 1fa97b58..b8357fe8 100644 --- a/k8s-source/adapters/replicationcontroller_test.go +++ b/k8s-source/adapters/replicationcontroller_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var replicationControllerYAML = ` diff --git a/k8s-source/adapters/resourcequota.go b/k8s-source/adapters/resourcequota.go index e0dbc30a..ca71086e 100644 --- a/k8s-source/adapters/resourcequota.go +++ b/k8s-source/adapters/resourcequota.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/resourcequota_test.go b/k8s-source/adapters/resourcequota_test.go index 57105817..2d52cb2a 100644 --- a/k8s-source/adapters/resourcequota_test.go +++ b/k8s-source/adapters/resourcequota_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var resourceQuotaYAML = ` diff --git a/k8s-source/adapters/role.go b/k8s-source/adapters/role.go index 52db8ecd..cb699a0c 100644 --- a/k8s-source/adapters/role.go +++ b/k8s-source/adapters/role.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/rbac/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/role_test.go b/k8s-source/adapters/role_test.go index 823334c4..9222f2f7 100644 --- a/k8s-source/adapters/role_test.go +++ b/k8s-source/adapters/role_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var RoleYAML = ` diff --git a/k8s-source/adapters/rolebinding.go b/k8s-source/adapters/rolebinding.go index 1e82d04a..bad7bdb8 100644 --- a/k8s-source/adapters/rolebinding.go +++ b/k8s-source/adapters/rolebinding.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/rbac/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -29,13 +29,6 @@ func roleBindingExtractor(resource *v1.RoleBinding, scope string) ([]*sdp.Linked Namespace: subject.Namespace, }.String(), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the subject (the group we're applying permissions - // to) won't affect the role or the binding - In: false, - // Changes to the binding will affect the subject - Out: true, - }, }) } @@ -61,13 +54,6 @@ func roleBindingExtractor(resource *v1.RoleBinding, scope string) ([]*sdp.Linked Query: resource.RoleRef.Name, Type: resource.RoleRef.Kind, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the role will affect the things bound to it since the - // role contains the permissions - In: true, - // Changes to the binding won't affect the role - Out: false, - }, }) return queries, nil diff --git a/k8s-source/adapters/rolebinding_test.go b/k8s-source/adapters/rolebinding_test.go index 3714cecb..f6e63e66 100644 --- a/k8s-source/adapters/rolebinding_test.go +++ b/k8s-source/adapters/rolebinding_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var roleBindingYAML = ` diff --git a/k8s-source/adapters/secret.go b/k8s-source/adapters/secret.go index 46e73b94..24e0538d 100644 --- a/k8s-source/adapters/secret.go +++ b/k8s-source/adapters/secret.go @@ -3,9 +3,9 @@ package adapters import ( "crypto/sha512" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/secret_test.go b/k8s-source/adapters/secret_test.go index 62b51a76..ba813cce 100644 --- a/k8s-source/adapters/secret_test.go +++ b/k8s-source/adapters/secret_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var secretYAML = ` diff --git a/k8s-source/adapters/service.go b/k8s-source/adapters/service.go index a88f65f4..6c49ed96 100644 --- a/k8s-source/adapters/service.go +++ b/k8s-source/adapters/service.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -22,12 +22,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer }), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the service - In: true, - Out: true, - }, }) } @@ -51,11 +45,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: ip, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always bidirectional - In: true, - Out: true, - }, }) } } @@ -68,11 +57,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: resource.Spec.ExternalName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always bidirectional - In: true, - Out: true, - }, }) } @@ -84,12 +68,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: resource.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The service causes the endpoint to be created, so changes to the - // service can affect the endpoint and vice versa - In: true, - Out: true, - }, }) for _, ingress := range resource.Status.LoadBalancer.Ingress { @@ -101,11 +79,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: ingress.IP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always bidirectional - In: true, - Out: true, - }, }) } @@ -117,11 +90,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: ingress.Hostname, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always bidirectional - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/service_test.go b/k8s-source/adapters/service_test.go index 26f502f4..d2fa89e1 100644 --- a/k8s-source/adapters/service_test.go +++ b/k8s-source/adapters/service_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var serviceYAML = ` diff --git a/k8s-source/adapters/serviceaccount.go b/k8s-source/adapters/serviceaccount.go index b7372430..c9b6b586 100644 --- a/k8s-source/adapters/serviceaccount.go +++ b/k8s-source/adapters/serviceaccount.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -19,13 +19,6 @@ func serviceAccountExtractor(resource *v1.ServiceAccount, scope string) ([]*sdp. Query: secret.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret will affect the service account and the - // things that use it - In: true, - // The service account cannot affect the secret - Out: false, - }, }) } @@ -37,13 +30,6 @@ func serviceAccountExtractor(resource *v1.ServiceAccount, scope string) ([]*sdp. Query: ipSecret.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret will affect the service account and the - // things that use it - In: true, - // The service account cannot affect the secret - Out: false, - }, }) } diff --git a/k8s-source/adapters/serviceaccount_test.go b/k8s-source/adapters/serviceaccount_test.go index 79bed8c7..4d2353cc 100644 --- a/k8s-source/adapters/serviceaccount_test.go +++ b/k8s-source/adapters/serviceaccount_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var serviceAccountYAML = ` diff --git a/k8s-source/adapters/shared_util.go b/k8s-source/adapters/shared_util.go index f8728fbb..8c0a46aa 100644 --- a/k8s-source/adapters/shared_util.go +++ b/k8s-source/adapters/shared_util.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/k8s-source/adapters/statefulset.go b/k8s-source/adapters/statefulset.go index 37f43264..e54e21d7 100644 --- a/k8s-source/adapters/statefulset.go +++ b/k8s-source/adapters/statefulset.go @@ -4,9 +4,9 @@ import ( v1 "k8s.io/api/apps/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) @@ -22,12 +22,6 @@ func statefulSetExtractor(resource *v1.StatefulSet, scope string) ([]*sdp.Linked Query: LabelSelectorToQuery(resource.Spec.Selector), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the stateful set - In: true, - Out: true, - }, }) if len(resource.Spec.VolumeClaimTemplates) > 0 { @@ -38,12 +32,6 @@ func statefulSetExtractor(resource *v1.StatefulSet, scope string) ([]*sdp.Linked Query: LabelSelectorToQuery(resource.Spec.Selector), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the stateful set - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/statefulset_test.go b/k8s-source/adapters/statefulset_test.go index 3e022447..fa6c9acc 100644 --- a/k8s-source/adapters/statefulset_test.go +++ b/k8s-source/adapters/statefulset_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var statefulSetYAML = ` diff --git a/k8s-source/adapters/storageclass.go b/k8s-source/adapters/storageclass.go index 8482d5d5..3c0f2d0c 100644 --- a/k8s-source/adapters/storageclass.go +++ b/k8s-source/adapters/storageclass.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/storage/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/storageclass_test.go b/k8s-source/adapters/storageclass_test.go index cd25744d..50a5aaa8 100644 --- a/k8s-source/adapters/storageclass_test.go +++ b/k8s-source/adapters/storageclass_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var storageClassYAML = ` diff --git a/k8s-source/adapters/volumeattachment.go b/k8s-source/adapters/volumeattachment.go index ce15ab65..d7bca3c6 100644 --- a/k8s-source/adapters/volumeattachment.go +++ b/k8s-source/adapters/volumeattachment.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/storage/v1" "k8s.io/client-go/kubernetes" ) @@ -19,11 +19,6 @@ func volumeAttachmentExtractor(resource *v1.VolumeAttachment, scope string) ([]* Query: *resource.Spec.Source.PersistentVolumeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the PV could affect the attachment and vice versa - In: true, - Out: true, - }, }) } @@ -35,12 +30,6 @@ func volumeAttachmentExtractor(resource *v1.VolumeAttachment, scope string) ([]* Query: resource.Spec.NodeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the node could affect the attachment and vice - // versa - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/volumeattachment_test.go b/k8s-source/adapters/volumeattachment_test.go index 094bd1d9..b835753c 100644 --- a/k8s-source/adapters/volumeattachment_test.go +++ b/k8s-source/adapters/volumeattachment_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var volumeAttachmentYAML = ` diff --git a/k8s-source/build/package/Dockerfile b/k8s-source/build/package/Dockerfile index 0360a8dd..c9f3c6a8 100644 --- a/k8s-source/build/package/Dockerfile +++ b/k8s-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source k8s-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source k8s-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/k8s-source/cmd/root.go b/k8s-source/cmd/root.go index 9a93677f..d6385bc9 100644 --- a/k8s-source/cmd/root.go +++ b/k8s-source/cmd/root.go @@ -16,12 +16,12 @@ import ( "time" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/k8s-source/adapters" "github.com/overmindtech/cli/k8s-source/proc" - "github.com/overmindtech/cli/logging" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/logging" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -383,7 +383,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "/etc/srcman/config/source.yaml", "config file path") rootCmd.PersistentFlags().StringVar(&logLevel, "log", "info", "Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace") - rootCmd.PersistentFlags().Int("health-check-port", 8080, "The port on which to serve health check endpoints (/healthz/alive, /healthz/ready, /healthz)") + rootCmd.PersistentFlags().Int("health-check-port", 8080, "The port on which to serve health check endpoints (/healthz/alive, /healthz/ready)") // engine flags discovery.AddEngineFlags(rootCmd) diff --git a/knowledge/discover.go b/knowledge/discover.go new file mode 100644 index 00000000..5e92dbf6 --- /dev/null +++ b/knowledge/discover.go @@ -0,0 +1,343 @@ +package knowledge + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" +) + +// KnowledgeFile represents a discovered and validated knowledge file +type KnowledgeFile struct { + Name string + Description string + Content string // markdown body only (excluding frontmatter) + FileName string // path relative to .overmind/knowledge/ +} + +// Warning represents a validation or parsing issue with a knowledge file +type Warning struct { + Path string // relative path within .overmind/knowledge/ + Reason string +} + +// frontmatter represents the YAML frontmatter structure +type frontmatter struct { + Name string `yaml:"name"` + Description string `yaml:"description"` +} + +// nameRegex validates knowledge file names (kebab-case: lowercase letters, digits, hyphens) +// Must start with a letter, end with letter or digit, 1-64 chars total +var nameRegex = regexp.MustCompile(`^[a-z]([a-z0-9-]*[a-z0-9])?$`) + +const ( + // maxFileSize is the maximum allowed size for a knowledge file (10MB) + // This prevents memory exhaustion and excessive API payload sizes + maxFileSize = 10 * 1024 * 1024 // 10MB +) + +// Discover walks the knowledge directory and discovers all valid knowledge files +// Returns valid files and any warnings encountered during discovery +func Discover(knowledgeDir string) ([]KnowledgeFile, []Warning) { + var files []KnowledgeFile + var warnings []Warning + + // Check if directory exists + if _, err := os.Stat(knowledgeDir); os.IsNotExist(err) { + return files, warnings + } + + // Collect all markdown files first for deterministic ordering + type fileInfo struct { + path string + relPath string + } + var mdFiles []fileInfo + + err := filepath.WalkDir(knowledgeDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + // Warn about directories/files we can't access + relPath, _ := filepath.Rel(knowledgeDir, path) + warnings = append(warnings, Warning{ + Path: relPath, + Reason: fmt.Sprintf("cannot access: %v", err), + }) + return nil // Continue walking + } + + // Skip directories + if d.IsDir() { + return nil + } + + // Only process .md files + if !strings.HasSuffix(d.Name(), ".md") { + return nil + } + + relPath, err := filepath.Rel(knowledgeDir, path) + if err != nil { + return err + } + + mdFiles = append(mdFiles, fileInfo{ + path: path, + relPath: relPath, + }) + + return nil + }) + + if err != nil { + warnings = append(warnings, Warning{ + Path: "", + Reason: fmt.Sprintf("error walking directory: %v", err), + }) + return files, warnings + } + + // Sort files lexicographically for deterministic processing + sort.Slice(mdFiles, func(i, j int) bool { + return mdFiles[i].relPath < mdFiles[j].relPath + }) + + // Track seen names for deduplication + seenNames := make(map[string]string) // name -> first file path + + // Process each file + for _, f := range mdFiles { + kf, warn := processFile(f.path, f.relPath) + if warn != nil { + warnings = append(warnings, *warn) + continue + } + + // Check for duplicate names + if firstPath, exists := seenNames[kf.Name]; exists { + warnings = append(warnings, Warning{ + Path: f.relPath, + Reason: fmt.Sprintf("duplicate name %q (already loaded from %q)", kf.Name, firstPath), + }) + continue + } + + seenNames[kf.Name] = f.relPath + files = append(files, *kf) + } + + return files, warnings +} + +// processFile reads and validates a single knowledge file +func processFile(path, relPath string) (*KnowledgeFile, *Warning) { + // Check file size before reading + fileInfo, err := os.Stat(path) + if err != nil { + return nil, &Warning{ + Path: relPath, + Reason: fmt.Sprintf("cannot stat file: %v", err), + } + } + + if fileInfo.Size() > maxFileSize { + return nil, &Warning{ + Path: relPath, + Reason: fmt.Sprintf("file size %d bytes exceeds maximum allowed size of %d bytes", fileInfo.Size(), maxFileSize), + } + } + + // Read file content + content, err := os.ReadFile(path) + if err != nil { + return nil, &Warning{ + Path: relPath, + Reason: fmt.Sprintf("cannot read file: %v", err), + } + } + + // Parse frontmatter + name, description, body, err := parseFrontmatter(string(content)) + if err != nil { + return nil, &Warning{ + Path: relPath, + Reason: err.Error(), + } + } + + // Validate name + if err := validateName(name); err != nil { + return nil, &Warning{ + Path: relPath, + Reason: err.Error(), + } + } + + // Validate description + if err := validateDescription(description); err != nil { + return nil, &Warning{ + Path: relPath, + Reason: err.Error(), + } + } + + return &KnowledgeFile{ + Name: name, + Description: description, + Content: body, + FileName: relPath, + }, nil +} + +// parseFrontmatter extracts YAML frontmatter from markdown content +// Returns name, description, body (without frontmatter), and any error +func parseFrontmatter(content string) (string, string, string, error) { + // Frontmatter must start at the beginning of the file + if !strings.HasPrefix(content, "---\n") && !strings.HasPrefix(content, "---\r\n") { + return "", "", "", fmt.Errorf("frontmatter is required (must start with ---)") + } + + // Determine opening delimiter length + startIdx := 4 // "---\n" + if strings.HasPrefix(content, "---\r\n") { + startIdx = 5 // "---\r\n" + } + + // Find the closing delimiter + remaining := content[startIdx:] + + // Handle edge case: empty frontmatter where second --- is immediately after first + if strings.HasPrefix(remaining, "---\n") || strings.HasPrefix(remaining, "---\r\n") { + bodyStartIdx := startIdx + 4 // "---\n" + if strings.HasPrefix(remaining, "---\r\n") { + bodyStartIdx = startIdx + 5 // "---\r\n" + } + body := strings.TrimLeft(content[bodyStartIdx:], "\n\r") + + // Empty frontmatter will result in empty name/description which will fail validation + var fm frontmatter + return fm.Name, fm.Description, body, nil + } + + // Find closing delimiter and track which type we found + var endIdx int + var closingDelimLen int + + // Try CRLF first (more specific), then LF + endIdx = strings.Index(remaining, "\n---\r\n") + if endIdx != -1 { + closingDelimLen = 6 // "\n---\r\n" + } else { + endIdx = strings.Index(remaining, "\n---\n") + if endIdx != -1 { + closingDelimLen = 5 // "\n---\n" + } else { + // Check for closing delimiter at end of file (more specific first) + if strings.HasSuffix(remaining, "\r\n---") { + endIdx = len(remaining) - 5 + closingDelimLen = 5 // "\r\n---" (no trailing newline) + } else if strings.HasSuffix(remaining, "\n---") { + endIdx = len(remaining) - 4 + closingDelimLen = 4 // "\n---" (no trailing newline) + } else { + return "", "", "", fmt.Errorf("frontmatter closing delimiter (---) not found") + } + } + } + + // Extract YAML content + yamlContent := remaining[:endIdx] + + // Parse YAML with strict mode (unknown fields will cause error) + var fm frontmatter + decoder := yaml.NewDecoder(strings.NewReader(yamlContent)) + decoder.KnownFields(true) // Reject unknown fields + if err := decoder.Decode(&fm); err != nil { + if strings.Contains(err.Error(), "field") && strings.Contains(err.Error(), "not found") { + return "", "", "", fmt.Errorf("only 'name' and 'description' fields are allowed in frontmatter") + } + return "", "", "", fmt.Errorf("invalid YAML in frontmatter: %w", err) + } + + // Extract body using the correct offset for the delimiter type found + bodyStartIdx := startIdx + endIdx + closingDelimLen + if bodyStartIdx > len(content) { + bodyStartIdx = len(content) + } + body := strings.TrimLeft(content[bodyStartIdx:], "\n\r") + + // Trim whitespace from name and description as per validation + return strings.TrimSpace(fm.Name), strings.TrimSpace(fm.Description), body, nil +} + +// validateName checks if the name meets the specification requirements +func validateName(name string) error { + name = strings.TrimSpace(name) + + if name == "" { + return fmt.Errorf("name is required") + } + + if len(name) > 64 { + return fmt.Errorf("name must be 64 characters or less") + } + + if !nameRegex.MatchString(name) { + return fmt.Errorf("name must use kebab-case (lowercase letters, digits, hyphens; start with letter, end with letter or digit)") + } + + return nil +} + +// validateDescription checks if the description meets the specification requirements +func validateDescription(description string) error { + description = strings.TrimSpace(description) + + if description == "" { + return fmt.Errorf("description is required") + } + + if len(description) > 1024 { + return fmt.Errorf("description must be 1024 characters or less") + } + + return nil +} + +// DiscoverAndConvert discovers knowledge files and converts them to SDP Knowledge messages. +// This is a convenience function that combines discovery, warning logging, and conversion +// to reduce code duplication across commands. +func DiscoverAndConvert(ctx context.Context, knowledgeDir string) []*sdp.Knowledge { + knowledgeFiles, warnings := Discover(knowledgeDir) + + // Log warnings + for _, w := range warnings { + log.WithContext(ctx).Warnf("Warning: skipping knowledge file %q: %s", w.Path, w.Reason) + } + + // Convert to SDP Knowledge messages + sdpKnowledge := make([]*sdp.Knowledge, len(knowledgeFiles)) + for i, kf := range knowledgeFiles { + sdpKnowledge[i] = &sdp.Knowledge{ + Name: kf.Name, + Description: kf.Description, + Content: kf.Content, + FileName: kf.FileName, + } + } + + // Log when knowledge files are loaded + if len(knowledgeFiles) > 0 { + log.WithContext(ctx).WithField("knowledgeCount", len(knowledgeFiles)).Info("Loaded knowledge files") + } + + return sdpKnowledge +} diff --git a/knowledge/discover_test.go b/knowledge/discover_test.go new file mode 100644 index 00000000..2e8d21b0 --- /dev/null +++ b/knowledge/discover_test.go @@ -0,0 +1,662 @@ +package knowledge + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestDiscover_EmptyDirectory(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + files, warnings := Discover(knowledgeDir) + + if len(files) != 0 { + t.Errorf("expected 0 files, got %d", len(files)) + } + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d", len(warnings)) + } +} + +func TestDiscover_DirectoryDoesNotExist(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "nonexistent") + + files, warnings := Discover(knowledgeDir) + + if len(files) != 0 { + t.Errorf("expected 0 files, got %d", len(files)) + } + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d", len(warnings)) + } +} + +func TestDiscover_ValidFiles(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create valid files at root + writeFile(t, filepath.Join(knowledgeDir, "aws-s3.md"), `--- +name: aws-s3-security +description: Security best practices for S3 buckets +--- +# AWS S3 Security +Content here. +`) + + // Create valid file in subfolder + subdir := filepath.Join(knowledgeDir, "cloud") + err = os.Mkdir(subdir, 0755) + if err != nil { + t.Fatal(err) + } + writeFile(t, filepath.Join(subdir, "gcp.md"), `--- +name: gcp-compute +description: GCP Compute Engine guidelines +--- +# GCP Compute +Content here. +`) + + files, warnings := Discover(knowledgeDir) + + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d: %v", len(warnings), warnings) + } + if len(files) != 2 { + t.Fatalf("expected 2 files, got %d", len(files)) + } + + // Check first file (lexicographic order) + if files[0].Name != "aws-s3-security" { + t.Errorf("expected name 'aws-s3-security', got %q", files[0].Name) + } + if files[0].Description != "Security best practices for S3 buckets" { + t.Errorf("unexpected description: %q", files[0].Description) + } + if files[0].FileName != "aws-s3.md" { + t.Errorf("expected fileName 'aws-s3.md', got %q", files[0].FileName) + } + if files[0].Content != "# AWS S3 Security\nContent here.\n" { + t.Errorf("unexpected content: %q", files[0].Content) + } + + // Check second file + if files[1].Name != "gcp-compute" { + t.Errorf("expected name 'gcp-compute', got %q", files[1].Name) + } + if files[1].FileName != filepath.Join("cloud", "gcp.md") { + t.Errorf("expected fileName 'cloud/gcp.md', got %q", files[1].FileName) + } +} + +func TestDiscover_NonMarkdownFilesSkipped(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create non-markdown files + writeFile(t, filepath.Join(knowledgeDir, "readme.txt"), "This is a text file") + writeFile(t, filepath.Join(knowledgeDir, "config.yaml"), "key: value") + writeFile(t, filepath.Join(knowledgeDir, "script.sh"), "#!/bin/bash") + + // Create one valid markdown file + writeFile(t, filepath.Join(knowledgeDir, "valid.md"), `--- +name: valid-file +description: A valid knowledge file +--- +Content +`) + + files, warnings := Discover(knowledgeDir) + + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d: %v", len(warnings), warnings) + } + if len(files) != 1 { + t.Errorf("expected 1 file, got %d", len(files)) + } +} + +func TestDiscover_NestedSubfolders(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + + // Create nested directory structure + deepDir := filepath.Join(knowledgeDir, "cloud", "aws", "services") + err := os.MkdirAll(deepDir, 0755) + if err != nil { + t.Fatal(err) + } + + writeFile(t, filepath.Join(deepDir, "s3.md"), `--- +name: deep-s3 +description: Deeply nested file +--- +Content +`) + + files, warnings := Discover(knowledgeDir) + + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d: %v", len(warnings), warnings) + } + if len(files) != 1 { + t.Fatalf("expected 1 file, got %d", len(files)) + } + expectedPath := filepath.Join("cloud", "aws", "services", "s3.md") + if files[0].FileName != expectedPath { + t.Errorf("expected fileName %q, got %q", expectedPath, files[0].FileName) + } +} + +func TestParseFrontmatter_Valid(t *testing.T) { + content := `--- +name: test-file +description: Test description +--- +# Markdown content +Here is some content. +` + + name, desc, body, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "test-file" { + t.Errorf("expected name 'test-file', got %q", name) + } + if desc != "Test description" { + t.Errorf("expected description 'Test description', got %q", desc) + } + if body != "# Markdown content\nHere is some content.\n" { + t.Errorf("unexpected body: %q", body) + } +} + +func TestParseFrontmatter_CRLF(t *testing.T) { + // Test with Windows-style CRLF line endings + content := "---\r\nname: windows-file\r\ndescription: File with CRLF endings\r\n---\r\n# Windows content\r\nWith CRLF.\r\n" + + name, desc, body, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "windows-file" { + t.Errorf("expected name 'windows-file', got %q", name) + } + if desc != "File with CRLF endings" { + t.Errorf("expected description 'File with CRLF endings', got %q", desc) + } + // Body should have CRLF stripped by TrimLeft + if !strings.Contains(body, "Windows content") { + t.Errorf("unexpected body: %q", body) + } +} + +func TestParseFrontmatter_CRLFAtEOF(t *testing.T) { + // Test CRLF with frontmatter at end of file (no trailing content) + content := "---\r\nname: eof-test\r\ndescription: Frontmatter at EOF\r\n---" + + name, desc, _, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "eof-test" { + t.Errorf("expected name 'eof-test', got %q", name) + } + if desc != "Frontmatter at EOF" { + t.Errorf("expected description 'Frontmatter at EOF', got %q", desc) + } +} + +func TestParseFrontmatter_MixedLineEndings(t *testing.T) { + // Test with LF in frontmatter but CRLF in closing delimiter + content := "---\nname: mixed-file\ndescription: Mixed line endings\n---\r\n# Content\nHere.\n" + + name, desc, body, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "mixed-file" { + t.Errorf("expected name 'mixed-file', got %q", name) + } + if desc != "Mixed line endings" { + t.Errorf("expected description 'Mixed line endings', got %q", desc) + } + if !strings.Contains(body, "Content") { + t.Errorf("unexpected body: %q", body) + } +} + +func TestParseFrontmatter_Whitespace(t *testing.T) { + // Test that whitespace is trimmed from name and description + content := `--- +name: whitespace-name +description: Lots of whitespace +--- +Content +` + + name, desc, _, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "whitespace-name" { + t.Errorf("expected trimmed name 'whitespace-name', got %q", name) + } + if desc != "Lots of whitespace" { + t.Errorf("expected trimmed description 'Lots of whitespace', got %q", desc) + } +} + +func TestParseFrontmatter_MissingFrontmatter(t *testing.T) { + content := `# Just markdown content +No frontmatter here. +` + + _, _, _, err := parseFrontmatter(content) + + if err == nil { + t.Error("expected error for missing frontmatter") + } +} + +func TestParseFrontmatter_EmptyFrontmatter(t *testing.T) { + content := `--- +--- +Content +` + + name, desc, _, err := parseFrontmatter(content) + + // Empty frontmatter parses successfully but will fail validation + if err != nil { + t.Fatalf("unexpected parse error: %v", err) + } + if name != "" || desc != "" { + t.Error("expected empty name and description") + } +} + +func TestParseFrontmatter_UnknownFields(t *testing.T) { + content := `--- +name: test +description: Test +license: MIT +author: Someone +--- +Content +` + + _, _, _, err := parseFrontmatter(content) + + if err == nil { + t.Error("expected error for unknown fields") + } + if err != nil && err.Error() != "only 'name' and 'description' fields are allowed in frontmatter" { + t.Errorf("unexpected error message: %v", err) + } +} + +func TestParseFrontmatter_InvalidYAML(t *testing.T) { + content := `--- +name: test +description: [unclosed bracket +--- +Content +` + + _, _, _, err := parseFrontmatter(content) + + if err == nil { + t.Error("expected error for invalid YAML") + } +} + +func TestParseFrontmatter_NoClosingDelimiter(t *testing.T) { + content := `--- +name: test +description: No closing delimiter +` + + _, _, _, err := parseFrontmatter(content) + + if err == nil { + t.Error("expected error for missing closing delimiter") + } +} + +func TestValidateName_Valid(t *testing.T) { + validNames := []string{ + "a", + "a1", + "aws-s3-security", + "kubernetes-resource-limits", + "test123", + "a-b-c-1-2-3", + } + + for _, name := range validNames { + err := validateName(name) + if err != nil { + t.Errorf("expected %q to be valid, got error: %v", name, err) + } + } +} + +func TestValidateName_Invalid(t *testing.T) { + tests := []struct { + name string + expectedErr string + }{ + {"", "name is required"}, + {" ", "name is required"}, + {"AWS-S3", "name must use kebab-case"}, + {"-leading-hyphen", "name must use kebab-case"}, + {"trailing-hyphen-", "name must use kebab-case"}, + {"123-starts-with-digit", "name must use kebab-case"}, + {"has_underscores", "name must use kebab-case"}, + {"has spaces", "name must use kebab-case"}, + {"Capital-Letter", "name must use kebab-case"}, + {string(make([]byte, 65)), "name must be 64 characters or less"}, // 65 chars + } + + for _, tt := range tests { + err := validateName(tt.name) + if err == nil { + t.Errorf("expected %q to be invalid", tt.name) + } else if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("for name %q, expected error containing %q, got %q", tt.name, tt.expectedErr, err.Error()) + } + } +} + +func TestValidateDescription_Valid(t *testing.T) { + validDescs := []string{ + "A", + "Short description", + string(make([]byte, 1024)), // exactly 1024 chars + } + + for _, desc := range validDescs { + err := validateDescription(desc) + if err != nil { + t.Errorf("expected %q to be valid, got error: %v", desc, err) + } + } +} + +func TestValidateDescription_Invalid(t *testing.T) { + tests := []struct { + desc string + expectedErr string + }{ + {"", "description is required"}, + {" ", "description is required"}, + {string(make([]byte, 1025)), "description must be 1024 characters or less"}, + } + + for _, tt := range tests { + err := validateDescription(tt.desc) + if err == nil { + t.Errorf("expected description to be invalid") + } else if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("expected error containing %q, got %q", tt.expectedErr, err.Error()) + } + } +} + +func TestDiscover_Deduplication(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create two files with same name + writeFile(t, filepath.Join(knowledgeDir, "aws-s3.md"), `--- +name: duplicate-name +description: First file +--- +First +`) + + writeFile(t, filepath.Join(knowledgeDir, "s3-aws.md"), `--- +name: duplicate-name +description: Second file +--- +Second +`) + + files, warnings := Discover(knowledgeDir) + + if len(files) != 1 { + t.Errorf("expected 1 file (first wins), got %d", len(files)) + } + if len(warnings) != 1 { + t.Fatalf("expected 1 warning for duplicate, got %d", len(warnings)) + } + + // First file (lexicographic order) should win + if files[0].Description != "First file" { + t.Errorf("expected first file to win, got description: %q", files[0].Description) + } + + // Check warning message + if !strings.Contains(warnings[0].Reason, "duplicate name") { + t.Errorf("expected warning about duplicate name, got: %q", warnings[0].Reason) + } + if !strings.Contains(warnings[0].Reason, "aws-s3.md") { + t.Errorf("expected warning to mention first file, got: %q", warnings[0].Reason) + } +} + +func TestDiscover_DuplicateInSubfolder(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + + subdir := filepath.Join(knowledgeDir, "cloud") + err := os.MkdirAll(subdir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create files with same name in different folders + writeFile(t, filepath.Join(knowledgeDir, "aws.md"), `--- +name: aws-service +description: Root file +--- +Root +`) + + writeFile(t, filepath.Join(subdir, "aws.md"), `--- +name: aws-service +description: Subfolder file +--- +Subfolder +`) + + files, warnings := Discover(knowledgeDir) + + if len(files) != 1 { + t.Errorf("expected 1 file, got %d", len(files)) + } + if len(warnings) != 1 { + t.Errorf("expected 1 warning for duplicate, got %d", len(warnings)) + } +} + +func TestDiscover_InvalidFilesProduceWarnings(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Invalid name + writeFile(t, filepath.Join(knowledgeDir, "invalid-name.md"), `--- +name: INVALID-NAME +description: Invalid name with uppercase +--- +Content +`) + + // Missing description + writeFile(t, filepath.Join(knowledgeDir, "no-desc.md"), `--- +name: no-description +--- +Content +`) + + // Invalid frontmatter + writeFile(t, filepath.Join(knowledgeDir, "bad-yaml.md"), `Not yaml frontmatter +`) + + // Valid file + writeFile(t, filepath.Join(knowledgeDir, "valid.md"), `--- +name: valid-file +description: This one is valid +--- +Content +`) + + files, warnings := Discover(knowledgeDir) + + if len(files) != 1 { + t.Errorf("expected 1 valid file, got %d", len(files)) + } + if len(warnings) != 3 { + t.Fatalf("expected 3 warnings, got %d: %v", len(warnings), warnings) + } + + // Check that all warnings have paths and reasons + for _, w := range warnings { + if w.Path == "" { + t.Error("warning path should not be empty") + } + if w.Reason == "" { + t.Error("warning reason should not be empty") + } + } +} + +func TestDiscover_FileSizeLimit(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create a file that exceeds the size limit + // Generate content larger than 10MB + largeContent := "---\nname: large-file\ndescription: Too large\n---\n" + largeContent += strings.Repeat("x", 11*1024*1024) // 11MB of content + + writeFile(t, filepath.Join(knowledgeDir, "large.md"), largeContent) + + // Create a valid small file + writeFile(t, filepath.Join(knowledgeDir, "small.md"), `--- +name: small-file +description: Normal size +--- +Content +`) + + files, warnings := Discover(knowledgeDir) + + if len(files) != 1 { + t.Errorf("expected 1 valid file, got %d", len(files)) + } + if len(warnings) != 1 { + t.Fatalf("expected 1 warning for large file, got %d", len(warnings)) + } + + if !strings.Contains(warnings[0].Reason, "exceeds maximum") { + t.Errorf("expected warning about file size, got: %q", warnings[0].Reason) + } +} + +func TestDiscover_LexicographicOrdering(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create files in non-alphabetical order + writeFile(t, filepath.Join(knowledgeDir, "zebra.md"), `--- +name: z-file +description: Last alphabetically +--- +Z +`) + + writeFile(t, filepath.Join(knowledgeDir, "apple.md"), `--- +name: a-file +description: First alphabetically +--- +A +`) + + writeFile(t, filepath.Join(knowledgeDir, "middle.md"), `--- +name: m-file +description: Middle alphabetically +--- +M +`) + + files, warnings := Discover(knowledgeDir) + + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d", len(warnings)) + } + if len(files) != 3 { + t.Fatalf("expected 3 files, got %d", len(files)) + } + + // Files should be processed in lexicographic order + if files[0].Name != "a-file" { + t.Errorf("expected first file to be 'a-file', got %q", files[0].Name) + } + if files[1].Name != "m-file" { + t.Errorf("expected second file to be 'm-file', got %q", files[1].Name) + } + if files[2].Name != "z-file" { + t.Errorf("expected third file to be 'z-file', got %q", files[2].Name) + } +} + +// Helper functions + +func writeFile(t *testing.T, path, content string) { + t.Helper() + err := os.WriteFile(path, []byte(content), 0644) + if err != nil { + t.Fatalf("failed to write file %s: %v", path, err) + } +} diff --git a/sources/aws/apigateway-api-key.go b/sources/aws/apigateway-api-key.go index 6cd112d4..eb6d6bfe 100644 --- a/sources/aws/apigateway-api-key.go +++ b/sources/aws/apigateway-api-key.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" awsshared "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" @@ -142,10 +142,6 @@ func (d *apiGatewayKeyWrapper) awsToSdpItem(apiKey types.ApiKey, scope string) ( Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/aws/apigateway-stage.go b/sources/aws/apigateway-stage.go index 196c841d..ef582d8b 100644 --- a/sources/aws/apigateway-stage.go +++ b/sources/aws/apigateway-stage.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" awsshared "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" @@ -167,10 +167,6 @@ func (d *apiGatewayStageWrapper) awsToSdpItem(stage types.Stage, scope, query st Query: restAPIID + "/" + *stage.DeploymentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -182,10 +178,6 @@ func (d *apiGatewayStageWrapper) awsToSdpItem(stage types.Stage, scope, query st Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) return item, nil diff --git a/sources/aws/base.go b/sources/aws/base.go index 00ce1909..1db67052 100644 --- a/sources/aws/base.go +++ b/sources/aws/base.go @@ -3,7 +3,7 @@ package aws import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/aws/errors.go b/sources/aws/errors.go index 2b028d50..2a46d4a2 100644 --- a/sources/aws/errors.go +++ b/sources/aws/errors.go @@ -6,7 +6,7 @@ import ( awsHttp "github.com/aws/smithy-go/transport/http" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // queryError takes an error and returns a sdp.QueryError. diff --git a/sources/aws/validation_test.go b/sources/aws/validation_test.go index 84c69e96..f79390d2 100644 --- a/sources/aws/validation_test.go +++ b/sources/aws/validation_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" ) diff --git a/sources/azure/build/package/Dockerfile b/sources/azure/build/package/Dockerfile index 8108bed6..3b607dc5 100644 --- a/sources/azure/build/package/Dockerfile +++ b/sources/azure/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source sources/azure/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source sources/azure/main.go FROM alpine:3.23 WORKDIR / diff --git a/sources/azure/clients/capacity-reservation-groups-client.go b/sources/azure/clients/capacity-reservation-groups-client.go new file mode 100644 index 00000000..2b0e2497 --- /dev/null +++ b/sources/azure/clients/capacity-reservation-groups-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_capacity_reservation_groups_client.go -package=mocks -source=capacity-reservation-groups-client.go + +// CapacityReservationGroupsPager is a type alias for the generic Pager interface with capacity reservation group response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type CapacityReservationGroupsPager = Pager[armcompute.CapacityReservationGroupsClientListByResourceGroupResponse] + +// CapacityReservationGroupsClient is an interface for interacting with Azure capacity reservation groups +type CapacityReservationGroupsClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions) CapacityReservationGroupsPager + Get(ctx context.Context, resourceGroupName string, capacityReservationGroupName string, options *armcompute.CapacityReservationGroupsClientGetOptions) (armcompute.CapacityReservationGroupsClientGetResponse, error) +} + +type capacityReservationGroupsClient struct { + client *armcompute.CapacityReservationGroupsClient +} + +func (a *capacityReservationGroupsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions) CapacityReservationGroupsPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *capacityReservationGroupsClient) Get(ctx context.Context, resourceGroupName string, capacityReservationGroupName string, options *armcompute.CapacityReservationGroupsClientGetOptions) (armcompute.CapacityReservationGroupsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, capacityReservationGroupName, options) +} + +// NewCapacityReservationGroupsClient creates a new CapacityReservationGroupsClient from the Azure SDK client +func NewCapacityReservationGroupsClient(client *armcompute.CapacityReservationGroupsClient) CapacityReservationGroupsClient { + return &capacityReservationGroupsClient{client: client} +} diff --git a/sources/azure/clients/dedicated-host-groups-client.go b/sources/azure/clients/dedicated-host-groups-client.go new file mode 100644 index 00000000..88b4c46a --- /dev/null +++ b/sources/azure/clients/dedicated-host-groups-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_dedicated_host_groups_client.go -package=mocks -source=dedicated-host-groups-client.go + +// DedicatedHostGroupsPager is a type alias for the generic Pager interface with dedicated host group response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type DedicatedHostGroupsPager = Pager[armcompute.DedicatedHostGroupsClientListByResourceGroupResponse] + +// DedicatedHostGroupsClient is an interface for interacting with Azure dedicated host groups +type DedicatedHostGroupsClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DedicatedHostGroupsClientListByResourceGroupOptions) DedicatedHostGroupsPager + Get(ctx context.Context, resourceGroupName string, dedicatedHostGroupName string, options *armcompute.DedicatedHostGroupsClientGetOptions) (armcompute.DedicatedHostGroupsClientGetResponse, error) +} + +type dedicatedHostGroupsClient struct { + client *armcompute.DedicatedHostGroupsClient +} + +func (a *dedicatedHostGroupsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DedicatedHostGroupsClientListByResourceGroupOptions) DedicatedHostGroupsPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *dedicatedHostGroupsClient) Get(ctx context.Context, resourceGroupName string, dedicatedHostGroupName string, options *armcompute.DedicatedHostGroupsClientGetOptions) (armcompute.DedicatedHostGroupsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, dedicatedHostGroupName, options) +} + +// NewDedicatedHostGroupsClient creates a new DedicatedHostGroupsClient from the Azure SDK client +func NewDedicatedHostGroupsClient(client *armcompute.DedicatedHostGroupsClient) DedicatedHostGroupsClient { + return &dedicatedHostGroupsClient{client: client} +} diff --git a/sources/azure/clients/disk-accesses-client.go b/sources/azure/clients/disk-accesses-client.go new file mode 100644 index 00000000..5f7a1b20 --- /dev/null +++ b/sources/azure/clients/disk-accesses-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_disk_accesses_client.go -package=mocks -source=disk-accesses-client.go + +// DiskAccessesPager is a type alias for the generic Pager interface with disk access response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type DiskAccessesPager = Pager[armcompute.DiskAccessesClientListByResourceGroupResponse] + +// DiskAccessesClient is an interface for interacting with Azure disk access +type DiskAccessesClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DiskAccessesClientListByResourceGroupOptions) DiskAccessesPager + Get(ctx context.Context, resourceGroupName string, diskAccessName string, options *armcompute.DiskAccessesClientGetOptions) (armcompute.DiskAccessesClientGetResponse, error) +} + +type diskAccessesClient struct { + client *armcompute.DiskAccessesClient +} + +func (a *diskAccessesClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DiskAccessesClientListByResourceGroupOptions) DiskAccessesPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *diskAccessesClient) Get(ctx context.Context, resourceGroupName string, diskAccessName string, options *armcompute.DiskAccessesClientGetOptions) (armcompute.DiskAccessesClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, diskAccessName, options) +} + +// NewDiskAccessesClient creates a new DiskAccessesClient from the Azure SDK client +func NewDiskAccessesClient(client *armcompute.DiskAccessesClient) DiskAccessesClient { + return &diskAccessesClient{client: client} +} \ No newline at end of file diff --git a/sources/azure/clients/galleries-client.go b/sources/azure/clients/galleries-client.go new file mode 100644 index 00000000..30640c8e --- /dev/null +++ b/sources/azure/clients/galleries-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_galleries_client.go -package=mocks -source=galleries-client.go + +// GalleriesPager is a type alias for the generic Pager interface with gallery response type. +type GalleriesPager = Pager[armcompute.GalleriesClientListByResourceGroupResponse] + +// GalleriesClient is an interface for interacting with Azure compute galleries +type GalleriesClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.GalleriesClientListByResourceGroupOptions) GalleriesPager + Get(ctx context.Context, resourceGroupName string, galleryName string, options *armcompute.GalleriesClientGetOptions) (armcompute.GalleriesClientGetResponse, error) +} + +type galleriesClient struct { + client *armcompute.GalleriesClient +} + +func (c *galleriesClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.GalleriesClientListByResourceGroupOptions) GalleriesPager { + return c.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (c *galleriesClient) Get(ctx context.Context, resourceGroupName string, galleryName string, options *armcompute.GalleriesClientGetOptions) (armcompute.GalleriesClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, galleryName, options) +} + +// NewGalleriesClient creates a new GalleriesClient from the Azure SDK client +func NewGalleriesClient(client *armcompute.GalleriesClient) GalleriesClient { + return &galleriesClient{client: client} +} diff --git a/sources/azure/clients/gallery-application-versions-client.go b/sources/azure/clients/gallery-application-versions-client.go new file mode 100644 index 00000000..c59744b1 --- /dev/null +++ b/sources/azure/clients/gallery-application-versions-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_gallery_application_versions_client.go -package=mocks -source=gallery-application-versions-client.go + +// GalleryApplicationVersionsPager is a type alias for the generic Pager interface with gallery application version response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type GalleryApplicationVersionsPager = Pager[armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse] + +// GalleryApplicationVersionsClient is an interface for interacting with Azure gallery application versions +type GalleryApplicationVersionsClient interface { + NewListByGalleryApplicationPager(resourceGroupName string, galleryName string, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) GalleryApplicationVersionsPager + Get(ctx context.Context, resourceGroupName string, galleryName string, galleryApplicationName string, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) +} + +type galleryApplicationVersionsClient struct { + client *armcompute.GalleryApplicationVersionsClient +} + +func (c *galleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName string, galleryName string, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) GalleryApplicationVersionsPager { + return c.client.NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options) +} + +func (c *galleryApplicationVersionsClient) Get(ctx context.Context, resourceGroupName string, galleryName string, galleryApplicationName string, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) +} + +// NewGalleryApplicationVersionsClient creates a new GalleryApplicationVersionsClient from the Azure SDK client +func NewGalleryApplicationVersionsClient(client *armcompute.GalleryApplicationVersionsClient) GalleryApplicationVersionsClient { + return &galleryApplicationVersionsClient{client: client} +} diff --git a/sources/azure/clients/gallery-images-client.go b/sources/azure/clients/gallery-images-client.go new file mode 100644 index 00000000..2ae750ab --- /dev/null +++ b/sources/azure/clients/gallery-images-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_gallery_images_client.go -package=mocks -source=gallery-images-client.go + +// GalleryImagesPager is a type alias for the generic Pager interface with gallery image response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type GalleryImagesPager = Pager[armcompute.GalleryImagesClientListByGalleryResponse] + +// GalleryImagesClient is an interface for interacting with Azure gallery image definitions +type GalleryImagesClient interface { + NewListByGalleryPager(resourceGroupName string, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) GalleryImagesPager + Get(ctx context.Context, resourceGroupName string, galleryName string, galleryImageName string, options *armcompute.GalleryImagesClientGetOptions) (armcompute.GalleryImagesClientGetResponse, error) +} + +type galleryImagesClient struct { + client *armcompute.GalleryImagesClient +} + +func (c *galleryImagesClient) NewListByGalleryPager(resourceGroupName string, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) GalleryImagesPager { + return c.client.NewListByGalleryPager(resourceGroupName, galleryName, options) +} + +func (c *galleryImagesClient) Get(ctx context.Context, resourceGroupName string, galleryName string, galleryImageName string, options *armcompute.GalleryImagesClientGetOptions) (armcompute.GalleryImagesClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, galleryName, galleryImageName, options) +} + +// NewGalleryImagesClient creates a new GalleryImagesClient from the Azure SDK client +func NewGalleryImagesClient(client *armcompute.GalleryImagesClient) GalleryImagesClient { + return &galleryImagesClient{client: client} +} diff --git a/sources/azure/clients/shared-gallery-images-client.go b/sources/azure/clients/shared-gallery-images-client.go new file mode 100644 index 00000000..5625f30b --- /dev/null +++ b/sources/azure/clients/shared-gallery-images-client.go @@ -0,0 +1,32 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_shared_gallery_images_client.go -package=mocks -source=shared-gallery-images-client.go + +type SharedGalleryImagesPager = Pager[armcompute.SharedGalleryImagesClientListResponse] + +type SharedGalleryImagesClient interface { + NewListPager(location string, galleryUniqueName string, options *armcompute.SharedGalleryImagesClientListOptions) SharedGalleryImagesPager + Get(ctx context.Context, location string, galleryUniqueName string, galleryImageName string, options *armcompute.SharedGalleryImagesClientGetOptions) (armcompute.SharedGalleryImagesClientGetResponse, error) +} + +type sharedGalleryImagesClient struct { + client *armcompute.SharedGalleryImagesClient +} + +func (c *sharedGalleryImagesClient) NewListPager(location string, galleryUniqueName string, options *armcompute.SharedGalleryImagesClientListOptions) SharedGalleryImagesPager { + return c.client.NewListPager(location, galleryUniqueName, options) +} + +func (c *sharedGalleryImagesClient) Get(ctx context.Context, location string, galleryUniqueName string, galleryImageName string, options *armcompute.SharedGalleryImagesClientGetOptions) (armcompute.SharedGalleryImagesClientGetResponse, error) { + return c.client.Get(ctx, location, galleryUniqueName, galleryImageName, options) +} + +func NewSharedGalleryImagesClient(client *armcompute.SharedGalleryImagesClient) SharedGalleryImagesClient { + return &sharedGalleryImagesClient{client: client} +} diff --git a/sources/azure/clients/snapshots-client.go b/sources/azure/clients/snapshots-client.go new file mode 100644 index 00000000..9070c3ce --- /dev/null +++ b/sources/azure/clients/snapshots-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_snapshots_client.go -package=mocks -source=snapshots-client.go + +// SnapshotsPager is a type alias for the generic Pager interface with snapshot response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type SnapshotsPager = Pager[armcompute.SnapshotsClientListByResourceGroupResponse] + +// SnapshotsClient is an interface for interacting with Azure snapshots +type SnapshotsClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.SnapshotsClientListByResourceGroupOptions) SnapshotsPager + Get(ctx context.Context, resourceGroupName string, snapshotName string, options *armcompute.SnapshotsClientGetOptions) (armcompute.SnapshotsClientGetResponse, error) +} + +type snapshotsClient struct { + client *armcompute.SnapshotsClient +} + +func (a *snapshotsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.SnapshotsClientListByResourceGroupOptions) SnapshotsPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *snapshotsClient) Get(ctx context.Context, resourceGroupName string, snapshotName string, options *armcompute.SnapshotsClientGetOptions) (armcompute.SnapshotsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, snapshotName, options) +} + +// NewSnapshotsClient creates a new SnapshotsClient from the Azure SDK client +func NewSnapshotsClient(client *armcompute.SnapshotsClient) SnapshotsClient { + return &snapshotsClient{client: client} +} diff --git a/sources/azure/cmd/root.go b/sources/azure/cmd/root.go index 2800afe0..ae641c37 100644 --- a/sources/azure/cmd/root.go +++ b/sources/azure/cmd/root.go @@ -9,15 +9,15 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/logging" + "github.com/overmindtech/cli/go/logging" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/sources/azure/proc" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) var cfgFile string diff --git a/sources/azure/docs/testing-federated-auth.md b/sources/azure/docs/testing-federated-auth.md index f19470c4..e1376808 100644 --- a/sources/azure/docs/testing-federated-auth.md +++ b/sources/azure/docs/testing-federated-auth.md @@ -102,7 +102,7 @@ INFO Sources initialized ```bash # Check health endpoint -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive # Expected: "ok" # Check logs for authentication method @@ -187,7 +187,7 @@ INFO Successfully verified subscription access # Should use environment variables, not Azure CLI # Verify it still works after Azure CLI logout -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive ``` ### Cleanup @@ -388,7 +388,7 @@ kubectl logs -l app=azure-source --tail=50 # Test health endpoint kubectl port-forward deployment/azure-source 8080:8080 & -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive ``` ### Troubleshooting @@ -580,7 +580,7 @@ kubectl logs -l app=azure-source --tail=50 # Check health kubectl port-forward deployment/azure-source 8080:8080 & -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive # Verify GCP token is available kubectl exec -it deployment/azure-source -- env | grep GOOGLE @@ -755,7 +755,7 @@ After completing any test scenario, perform these verification steps: kubectl port-forward deployment/azure-source 8080:8080 & # Check health -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive # Expected: "ok" ``` diff --git a/sources/azure/docs/usage.md b/sources/azure/docs/usage.md index 2afcafdb..796b90ed 100644 --- a/sources/azure/docs/usage.md +++ b/sources/azure/docs/usage.md @@ -331,10 +331,10 @@ The source exposes a health check endpoint: ```bash # Check health -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive # Response: "ok" (HTTP 200) if healthy -# Response: Error message (HTTP 500) if unhealthy +# Response: Error message (HTTP 503 Service Unavailable) if unhealthy ``` The health check verifies: diff --git a/sources/azure/integration-tests/authorization-role-assignment_test.go b/sources/azure/integration-tests/authorization-role-assignment_test.go index 8a15c3e8..b4548094 100644 --- a/sources/azure/integration-tests/authorization-role-assignment_test.go +++ b/sources/azure/integration-tests/authorization-role-assignment_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -272,12 +272,6 @@ func TestAuthorizationRoleAssignmentIntegration(t *testing.T) { if linkedQuery.GetQuery().GetScope() != subscriptionID { t.Errorf("Expected role definition link scope to be subscription ID %s, got %s", subscriptionID, linkedQuery.GetQuery().GetScope()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected role definition link BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected role definition link BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/integration-tests/batch-batch-accounts_test.go b/sources/azure/integration-tests/batch-batch-accounts_test.go index 6ef8d0aa..4aea0a93 100644 --- a/sources/azure/integration-tests/batch-batch-accounts_test.go +++ b/sources/azure/integration-tests/batch-batch-accounts_test.go @@ -18,9 +18,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -246,30 +246,6 @@ func TestBatchAccountIntegration(t *testing.T) { t.Errorf("Expected linked query method to be SEARCH for %s, got %s", linkedType, queryMethod) } } - - // Verify blast propagation - if liq.GetBlastPropagation() == nil { - t.Errorf("Expected blast propagation to be set for linked type %s", linkedType) - } else { - bp := liq.GetBlastPropagation() - if linkedType == azureshared.StorageAccount.String() { - // Storage account: In=true, Out=false (batch depends on storage) - if bp.GetIn() != true { - t.Errorf("Expected blast propagation In=true for storage account, got false") - } - if bp.GetOut() != false { - t.Errorf("Expected blast propagation Out=false for storage account, got true") - } - } else { - // Child resources: In=true, Out=true (tightly coupled) - if bp.GetIn() != true { - t.Errorf("Expected blast propagation In=true for %s, got false", linkedType) - } - if bp.GetOut() != true { - t.Errorf("Expected blast propagation Out=true for %s, got false", linkedType) - } - } - } } } diff --git a/sources/azure/integration-tests/compute-availability-set_test.go b/sources/azure/integration-tests/compute-availability-set_test.go index 874d0f6f..ffc52e92 100644 --- a/sources/azure/integration-tests/compute-availability-set_test.go +++ b/sources/azure/integration-tests/compute-availability-set_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -272,26 +272,12 @@ func TestComputeAvailabilitySetIntegration(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected VM link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected VM blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected VM blast propagation Out=false, got true") - } case azureshared.ComputeProximityPlacementGroup.String(): // PPG may or may not be present depending on availability set setup // Verify PPG link properties if present if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected PPG link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected PPG blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected PPG blast propagation Out=false, got true") - } } } diff --git a/sources/azure/integration-tests/compute-capacity-reservation-group_test.go b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go new file mode 100644 index 00000000..9509e715 --- /dev/null +++ b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go @@ -0,0 +1,290 @@ +package integrationtests + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + "k8s.io/utils/ptr" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +const ( + integrationTestCapacityReservationGroupName = "ovm-integ-test-capacity-reservation-group" +) + +func TestComputeCapacityReservationGroupIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + capacityReservationGroupsClient, err := armcompute.NewCapacityReservationGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Capacity Reservation Groups client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Setup", func(t *testing.T) { + ctx := t.Context() + + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create resource group: %v", err) + } + + err = createCapacityReservationGroup(ctx, capacityReservationGroupsClient, integrationTestResourceGroup, integrationTestCapacityReservationGroupName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create capacity reservation group: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + t.Run("GetCapacityReservationGroup", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving capacity reservation group %s in subscription %s, resource group %s", + integrationTestCapacityReservationGroupName, subscriptionID, integrationTestResourceGroup) + + capacityReservationGroupWrapper := manual.NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := capacityReservationGroupWrapper.Scopes()[0] + + capacityReservationGroupAdapter := sources.WrapperToAdapter(capacityReservationGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := capacityReservationGroupAdapter.Get(ctx, scope, integrationTestCapacityReservationGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + if uniqueAttrValue != integrationTestCapacityReservationGroupName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestCapacityReservationGroupName, uniqueAttrValue) + } + + log.Printf("Successfully retrieved capacity reservation group %s", integrationTestCapacityReservationGroupName) + }) + + t.Run("ListCapacityReservationGroups", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing capacity reservation groups in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + capacityReservationGroupWrapper := manual.NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := capacityReservationGroupWrapper.Scopes()[0] + + capacityReservationGroupAdapter := sources.WrapperToAdapter(capacityReservationGroupWrapper, sdpcache.NewNoOpCache()) + + listable, ok := capacityReservationGroupAdapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Failed to list capacity reservation groups: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one capacity reservation group, got %d", len(sdpItems)) + } + + var found bool + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestCapacityReservationGroupName { + found = true + break + } + } + + if !found { + t.Fatalf("Expected to find capacity reservation group %s in the list of capacity reservation groups", integrationTestCapacityReservationGroupName) + } + + log.Printf("Found %d capacity reservation groups in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for capacity reservation group %s", integrationTestCapacityReservationGroupName) + + capacityReservationGroupWrapper := manual.NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := capacityReservationGroupWrapper.Scopes()[0] + + capacityReservationGroupAdapter := sources.WrapperToAdapter(capacityReservationGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := capacityReservationGroupAdapter.Get(ctx, scope, integrationTestCapacityReservationGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeCapacityReservationGroup.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeCapacityReservationGroup.String(), sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for capacity reservation group %s", integrationTestCapacityReservationGroupName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for capacity reservation group %s", integrationTestCapacityReservationGroupName) + + capacityReservationGroupWrapper := manual.NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := capacityReservationGroupWrapper.Scopes()[0] + + capacityReservationGroupAdapter := sources.WrapperToAdapter(capacityReservationGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := capacityReservationGroupAdapter.Get(ctx, scope, integrationTestCapacityReservationGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for capacity reservation group %s", len(linkedQueries), integrationTestCapacityReservationGroupName) + + // Capacity reservation group may have zero or more linked queries (capacity reservations, VMs) depending on configuration + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + } + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + err := deleteCapacityReservationGroup(ctx, capacityReservationGroupsClient, integrationTestResourceGroup, integrationTestCapacityReservationGroupName) + if err != nil { + t.Fatalf("Failed to delete capacity reservation group: %v", err) + } + }) +} + +// createCapacityReservationGroup creates an Azure capacity reservation group resource (idempotent). +func createCapacityReservationGroup(ctx context.Context, client *armcompute.CapacityReservationGroupsClient, resourceGroupName, groupName, location string) error { + _, err := client.Get(ctx, resourceGroupName, groupName, nil) + if err == nil { + log.Printf("Capacity reservation group %s already exists, skipping creation", groupName) + return nil + } + + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode != http.StatusNotFound { + return fmt.Errorf("unexpected error checking capacity reservation group: %w", err) + } + + _, err = client.CreateOrUpdate(ctx, resourceGroupName, groupName, armcompute.CapacityReservationGroup{ + Location: ptr.To(location), + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-capacity-reservation-group"), + }, + }, nil) + if err != nil { + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Capacity reservation group %s already exists (conflict), skipping creation", groupName) + return nil + } + return fmt.Errorf("failed to create capacity reservation group: %w", err) + } + + log.Printf("Capacity reservation group %s created successfully", groupName) + return nil +} + +// deleteCapacityReservationGroup deletes an Azure capacity reservation group resource. +// Azure may return 202 Accepted for async delete; treat that as success. +func deleteCapacityReservationGroup(ctx context.Context, client *armcompute.CapacityReservationGroupsClient, resourceGroupName, groupName string) error { + _, err := client.Delete(ctx, resourceGroupName, groupName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) { + switch respErr.StatusCode { + case http.StatusNotFound: + log.Printf("Capacity reservation group %s not found, skipping deletion", groupName) + return nil + case http.StatusAccepted: + // Async delete accepted; resource deletion is in progress + log.Printf("Capacity reservation group %s delete accepted (202), teardown complete", groupName) + return nil + } + } + return fmt.Errorf("failed to delete capacity reservation group: %w", err) + } + + log.Printf("Capacity reservation group %s deleted successfully", groupName) + return nil +} diff --git a/sources/azure/integration-tests/compute-dedicated-host-group_test.go b/sources/azure/integration-tests/compute-dedicated-host-group_test.go new file mode 100644 index 00000000..89e1d475 --- /dev/null +++ b/sources/azure/integration-tests/compute-dedicated-host-group_test.go @@ -0,0 +1,285 @@ +package integrationtests + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + "k8s.io/utils/ptr" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +const ( + integrationTestDedicatedHostGroupName = "ovm-integ-test-dedicated-host-group" +) + +func TestComputeDedicatedHostGroupIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + dedicatedHostGroupsClient, err := armcompute.NewDedicatedHostGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Dedicated Host Groups client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Setup", func(t *testing.T) { + ctx := t.Context() + + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create resource group: %v", err) + } + + err = createDedicatedHostGroup(ctx, dedicatedHostGroupsClient, integrationTestResourceGroup, integrationTestDedicatedHostGroupName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create dedicated host group: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + t.Run("GetDedicatedHostGroup", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving dedicated host group %s in subscription %s, resource group %s", + integrationTestDedicatedHostGroupName, subscriptionID, integrationTestResourceGroup) + + dedicatedHostGroupWrapper := manual.NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := dedicatedHostGroupWrapper.Scopes()[0] + + dedicatedHostGroupAdapter := sources.WrapperToAdapter(dedicatedHostGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := dedicatedHostGroupAdapter.Get(ctx, scope, integrationTestDedicatedHostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + if uniqueAttrValue != integrationTestDedicatedHostGroupName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestDedicatedHostGroupName, uniqueAttrValue) + } + + log.Printf("Successfully retrieved dedicated host group %s", integrationTestDedicatedHostGroupName) + }) + + t.Run("ListDedicatedHostGroups", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing dedicated host groups in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + dedicatedHostGroupWrapper := manual.NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := dedicatedHostGroupWrapper.Scopes()[0] + + dedicatedHostGroupAdapter := sources.WrapperToAdapter(dedicatedHostGroupWrapper, sdpcache.NewNoOpCache()) + + listable, ok := dedicatedHostGroupAdapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Failed to list dedicated host groups: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one dedicated host group, got %d", len(sdpItems)) + } + + var found bool + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestDedicatedHostGroupName { + found = true + break + } + } + + if !found { + t.Fatalf("Expected to find dedicated host group %s in the list of dedicated host groups", integrationTestDedicatedHostGroupName) + } + + log.Printf("Found %d dedicated host groups in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for dedicated host group %s", integrationTestDedicatedHostGroupName) + + dedicatedHostGroupWrapper := manual.NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := dedicatedHostGroupWrapper.Scopes()[0] + + dedicatedHostGroupAdapter := sources.WrapperToAdapter(dedicatedHostGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := dedicatedHostGroupAdapter.Get(ctx, scope, integrationTestDedicatedHostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDedicatedHostGroup.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeDedicatedHostGroup.String(), sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for dedicated host group %s", integrationTestDedicatedHostGroupName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for dedicated host group %s", integrationTestDedicatedHostGroupName) + + dedicatedHostGroupWrapper := manual.NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := dedicatedHostGroupWrapper.Scopes()[0] + + dedicatedHostGroupAdapter := sources.WrapperToAdapter(dedicatedHostGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := dedicatedHostGroupAdapter.Get(ctx, scope, integrationTestDedicatedHostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for dedicated host group %s", len(linkedQueries), integrationTestDedicatedHostGroupName) + + // Dedicated host group may have zero or more linked queries (ComputeDedicatedHost) depending on whether hosts exist + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + } + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + err := deleteDedicatedHostGroup(ctx, dedicatedHostGroupsClient, integrationTestResourceGroup, integrationTestDedicatedHostGroupName) + if err != nil { + t.Fatalf("Failed to delete dedicated host group: %v", err) + } + }) +} + +// createDedicatedHostGroup creates an Azure dedicated host group resource (idempotent). +func createDedicatedHostGroup(ctx context.Context, client *armcompute.DedicatedHostGroupsClient, resourceGroupName, hostGroupName, location string) error { + _, err := client.Get(ctx, resourceGroupName, hostGroupName, nil) + if err == nil { + log.Printf("Dedicated host group %s already exists, skipping creation", hostGroupName) + return nil + } + + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode != http.StatusNotFound { + return fmt.Errorf("unexpected error checking dedicated host group: %w", err) + } + + _, err = client.CreateOrUpdate(ctx, resourceGroupName, hostGroupName, armcompute.DedicatedHostGroup{ + Location: ptr.To(location), + Properties: &armcompute.DedicatedHostGroupProperties{ + PlatformFaultDomainCount: ptr.To[int32](1), + }, + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-dedicated-host-group"), + }, + }, nil) + if err != nil { + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Dedicated host group %s already exists (conflict), skipping creation", hostGroupName) + return nil + } + return fmt.Errorf("failed to create dedicated host group: %w", err) + } + + log.Printf("Dedicated host group %s created successfully", hostGroupName) + return nil +} + +// deleteDedicatedHostGroup deletes an Azure dedicated host group resource. +func deleteDedicatedHostGroup(ctx context.Context, client *armcompute.DedicatedHostGroupsClient, resourceGroupName, hostGroupName string) error { + _, err := client.Delete(ctx, resourceGroupName, hostGroupName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Dedicated host group %s not found, skipping deletion", hostGroupName) + return nil + } + return fmt.Errorf("failed to delete dedicated host group: %w", err) + } + + log.Printf("Dedicated host group %s deleted successfully", hostGroupName) + return nil +} diff --git a/sources/azure/integration-tests/compute-disk-access_test.go b/sources/azure/integration-tests/compute-disk-access_test.go new file mode 100644 index 00000000..bea69ff7 --- /dev/null +++ b/sources/azure/integration-tests/compute-disk-access_test.go @@ -0,0 +1,356 @@ +package integrationtests + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + "k8s.io/utils/ptr" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +const ( + integrationTestDiskAccessName = "ovm-integ-test-disk-access" +) + +func TestComputeDiskAccessIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + diskAccessClient, err := armcompute.NewDiskAccessesClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Disk Accesses client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Setup", func(t *testing.T) { + ctx := t.Context() + + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create resource group: %v", err) + } + + err = createDiskAccess(ctx, diskAccessClient, integrationTestResourceGroup, integrationTestDiskAccessName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create disk access: %v", err) + } + + err = waitForDiskAccessAvailable(ctx, diskAccessClient, integrationTestResourceGroup, integrationTestDiskAccessName) + if err != nil { + t.Fatalf("Failed waiting for disk access to be available: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + t.Run("GetDiskAccess", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving disk access %s in subscription %s, resource group %s", + integrationTestDiskAccessName, subscriptionID, integrationTestResourceGroup) + + diskAccessWrapper := manual.NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := diskAccessWrapper.Scopes()[0] + + diskAccessAdapter := sources.WrapperToAdapter(diskAccessWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := diskAccessAdapter.Get(ctx, scope, integrationTestDiskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + if uniqueAttrValue != integrationTestDiskAccessName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestDiskAccessName, uniqueAttrValue) + } + + log.Printf("Successfully retrieved disk access %s", integrationTestDiskAccessName) + }) + + t.Run("ListDiskAccesses", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing disk accesses in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + diskAccessWrapper := manual.NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := diskAccessWrapper.Scopes()[0] + + diskAccessAdapter := sources.WrapperToAdapter(diskAccessWrapper, sdpcache.NewNoOpCache()) + + listable, ok := diskAccessAdapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Failed to list disk accesses: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one disk access, got %d", len(sdpItems)) + } + + var found bool + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestDiskAccessName { + found = true + break + } + } + + if !found { + t.Fatalf("Expected to find disk access %s in the list of disk accesses", integrationTestDiskAccessName) + } + + log.Printf("Found %d disk accesses in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for disk access %s", integrationTestDiskAccessName) + + diskAccessWrapper := manual.NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := diskAccessWrapper.Scopes()[0] + + diskAccessAdapter := sources.WrapperToAdapter(diskAccessWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := diskAccessAdapter.Get(ctx, scope, integrationTestDiskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDiskAccess.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeDiskAccess.String(), sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for disk access %s", integrationTestDiskAccessName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for disk access %s", integrationTestDiskAccessName) + + diskAccessWrapper := manual.NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := diskAccessWrapper.Scopes()[0] + + diskAccessAdapter := sources.WrapperToAdapter(diskAccessWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := diskAccessAdapter.Get(ctx, scope, integrationTestDiskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for disk access %s", len(linkedQueries), integrationTestDiskAccessName) + + // Disk access always has at least one linked query: ComputeDiskAccessPrivateEndpointConnection (SEARCH) + if len(linkedQueries) < 1 { + t.Errorf("Expected at least one linked item query (private endpoint connection), got %d", len(linkedQueries)) + } + + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + } + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + err := deleteDiskAccess(ctx, diskAccessClient, integrationTestResourceGroup, integrationTestDiskAccessName) + if err != nil { + t.Fatalf("Failed to delete disk access: %v", err) + } + }) +} + +// createDiskAccess creates an Azure disk access resource (idempotent). +func createDiskAccess(ctx context.Context, client *armcompute.DiskAccessesClient, resourceGroupName, diskAccessName, location string) error { + existing, err := client.Get(ctx, resourceGroupName, diskAccessName, nil) + if err == nil { + if existing.Properties != nil && existing.Properties.ProvisioningState != nil { + state := *existing.Properties.ProvisioningState + if state == "Succeeded" { + log.Printf("Disk access %s already exists with state %s, skipping creation", diskAccessName, state) + return nil + } + log.Printf("Disk access %s exists but in state %s, will wait for it", diskAccessName, state) + } else { + log.Printf("Disk access %s already exists, skipping creation", diskAccessName) + return nil + } + } + + poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, diskAccessName, armcompute.DiskAccess{ + Location: ptr.To(location), + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-disk-access"), + }, + }, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Disk access %s already exists (conflict), skipping creation", diskAccessName) + return nil + } + return fmt.Errorf("failed to begin creating disk access: %w", err) + } + + resp, err := poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to create disk access: %w", err) + } + + if resp.Properties != nil && resp.Properties.ProvisioningState != nil { + state := *resp.Properties.ProvisioningState + if state != "Succeeded" { + return fmt.Errorf("disk access provisioning state is %s, expected Succeeded", state) + } + log.Printf("Disk access %s created successfully with provisioning state: %s", diskAccessName, state) + } else { + log.Printf("Disk access %s created successfully", diskAccessName) + } + + return nil +} + +// waitForDiskAccessAvailable polls until the disk access is available via the Get API. +func waitForDiskAccessAvailable(ctx context.Context, client *armcompute.DiskAccessesClient, resourceGroupName, diskAccessName string) error { + maxAttempts := 20 + pollInterval := 5 * time.Second + + log.Printf("Waiting for disk access %s to be available via API...", diskAccessName) + + for attempt := 1; attempt <= maxAttempts; attempt++ { + resp, err := client.Get(ctx, resourceGroupName, diskAccessName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Disk access %s not yet available (attempt %d/%d), waiting %v...", diskAccessName, attempt, maxAttempts, pollInterval) + time.Sleep(pollInterval) + continue + } + return fmt.Errorf("error checking disk access availability: %w", err) + } + + if resp.Properties != nil && resp.Properties.ProvisioningState != nil { + state := *resp.Properties.ProvisioningState + if state == "Succeeded" { + log.Printf("Disk access %s is available with provisioning state: %s", diskAccessName, state) + return nil + } + if state == "Failed" { + return fmt.Errorf("disk access provisioning failed with state: %s", state) + } + log.Printf("Disk access %s provisioning state: %s (attempt %d/%d), waiting...", diskAccessName, state, attempt, maxAttempts) + time.Sleep(pollInterval) + continue + } + + log.Printf("Disk access %s is available", diskAccessName) + return nil + } + + return fmt.Errorf("timeout waiting for disk access %s to be available after %d attempts", diskAccessName, maxAttempts) +} + +// deleteDiskAccess deletes an Azure disk access resource. +func deleteDiskAccess(ctx context.Context, client *armcompute.DiskAccessesClient, resourceGroupName, diskAccessName string) error { + poller, err := client.BeginDelete(ctx, resourceGroupName, diskAccessName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Disk access %s not found, skipping deletion", diskAccessName) + return nil + } + return fmt.Errorf("failed to begin deleting disk access: %w", err) + } + + _, err = poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to delete disk access: %w", err) + } + + log.Printf("Disk access %s deleted successfully", diskAccessName) + return nil +} diff --git a/sources/azure/integration-tests/compute-disk-encryption-set_test.go b/sources/azure/integration-tests/compute-disk-encryption-set_test.go index a800dea6..4278e7b6 100644 --- a/sources/azure/integration-tests/compute-disk-encryption-set_test.go +++ b/sources/azure/integration-tests/compute-disk-encryption-set_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -281,16 +281,6 @@ func TestComputeDiskEncryptionSetIntegration(t *testing.T) { if query.GetScope() != scope { t.Errorf("Expected Key Vault link scope %s, got %s", scope, query.GetScope()) } - if liq.GetBlastPropagation() == nil { - t.Error("Key Vault linked item query has nil BlastPropagation") - } else { - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Key Vault BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Key Vault BlastPropagation.Out to be false") - } - } case azureshared.KeyVaultKey.String(): hasKeyVaultKeyLink = true if query.GetMethod() != sdp.QueryMethod_GET { @@ -303,16 +293,6 @@ func TestComputeDiskEncryptionSetIntegration(t *testing.T) { if query.GetScope() != scope { t.Errorf("Expected Key Vault Key link scope %s, got %s", scope, query.GetScope()) } - if liq.GetBlastPropagation() == nil { - t.Error("Key Vault Key linked item query has nil BlastPropagation") - } else { - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Key Vault Key BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Key Vault Key BlastPropagation.Out to be false") - } - } case azureshared.ManagedIdentityUserAssignedIdentity.String(): hasUserAssignedIdentityLink = true if query.GetMethod() != sdp.QueryMethod_GET { @@ -324,16 +304,6 @@ func TestComputeDiskEncryptionSetIntegration(t *testing.T) { if query.GetScope() != scope { t.Errorf("Expected User Assigned Identity link scope %s, got %s", scope, query.GetScope()) } - if liq.GetBlastPropagation() == nil { - t.Error("User Assigned Identity linked item query has nil BlastPropagation") - } else { - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected User Assigned Identity BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected User Assigned Identity BlastPropagation.Out to be false") - } - } case "dns": hasDNSLink = true if query.GetMethod() != sdp.QueryMethod_SEARCH { @@ -346,16 +316,6 @@ func TestComputeDiskEncryptionSetIntegration(t *testing.T) { if query.GetScope() != "global" { t.Errorf("Expected DNS link scope global, got %s", query.GetScope()) } - if liq.GetBlastPropagation() == nil { - t.Error("DNS linked item query has nil BlastPropagation") - } else { - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected DNS BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected DNS BlastPropagation.Out to be true") - } - } default: t.Errorf("Unexpected linked item type: %s", query.GetType()) } diff --git a/sources/azure/integration-tests/compute-disk_test.go b/sources/azure/integration-tests/compute-disk_test.go index 2932bbce..bd47dad0 100644 --- a/sources/azure/integration-tests/compute-disk_test.go +++ b/sources/azure/integration-tests/compute-disk_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -242,19 +242,8 @@ func TestComputeDiskIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } - - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) } }) }) diff --git a/sources/azure/integration-tests/compute-gallery-application-version_test.go b/sources/azure/integration-tests/compute-gallery-application-version_test.go new file mode 100644 index 00000000..28959583 --- /dev/null +++ b/sources/azure/integration-tests/compute-gallery-application-version_test.go @@ -0,0 +1,250 @@ +package integrationtests + +import ( + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +// Gallery application version integration tests require pre-existing Azure resources +// (gallery, gallery application, and gallery application version) because creating +// a version requires a source blob URL. Set these env vars to run the tests: +// +// AZURE_TEST_GALLERY_NAME - name of the gallery +// AZURE_TEST_GALLERY_APPLICATION_NAME - name of the gallery application +// AZURE_TEST_GALLERY_APPLICATION_VERSION - name of the gallery application version +// +// Optional: AZURE_TEST_GALLERY_RESOURCE_GROUP (defaults to overmind-integration-tests) +func getGalleryApplicationVersionTestConfig(t *testing.T) (resourceGroup, galleryName, applicationName, versionName string, skip bool) { + galleryName = os.Getenv("AZURE_TEST_GALLERY_NAME") + applicationName = os.Getenv("AZURE_TEST_GALLERY_APPLICATION_NAME") + versionName = os.Getenv("AZURE_TEST_GALLERY_APPLICATION_VERSION") + resourceGroup = os.Getenv("AZURE_TEST_GALLERY_RESOURCE_GROUP") + if resourceGroup == "" { + resourceGroup = integrationTestResourceGroup + } + if galleryName == "" || applicationName == "" || versionName == "" { + t.Skip("Skipping gallery application version integration test: set AZURE_TEST_GALLERY_NAME, AZURE_TEST_GALLERY_APPLICATION_NAME, and AZURE_TEST_GALLERY_APPLICATION_VERSION to run") + return "", "", "", "", true + } + return resourceGroup, galleryName, applicationName, versionName, false +} + +func TestComputeGalleryApplicationVersionIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + resourceGroup, galleryName, applicationName, versionName, skip := getGalleryApplicationVersionTestConfig(t) + if skip { + return + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + galleryApplicationVersionsClient, err := armcompute.NewGalleryApplicationVersionsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Gallery Application Versions client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Run", func(t *testing.T) { + ctx := t.Context() + + // Ensure resource group exists (may be used for pre-created gallery) + err := createResourceGroup(ctx, rgClient, resourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create/verify resource group: %v", err) + } + + t.Run("GetGalleryApplicationVersion", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving gallery application version %s/%s/%s in subscription %s, resource group %s", + galleryName, applicationName, versionName, subscriptionID, resourceGroup) + + wrapper := manual.NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}, + ) + scope := wrapper.Scopes()[0] + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + query := shared.CompositeLookupKey(galleryName, applicationName, versionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + expectedUniqueAttr := shared.CompositeLookupKey(galleryName, applicationName, versionName) + if uniqueAttrValue != expectedUniqueAttr { + t.Fatalf("Expected unique attribute value to be %s, got %s", expectedUniqueAttr, uniqueAttrValue) + } + + log.Printf("Successfully retrieved gallery application version %s", versionName) + }) + + t.Run("SearchGalleryApplicationVersions", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Searching gallery application versions for gallery %s, application %s in subscription %s, resource group %s", + galleryName, applicationName, subscriptionID, resourceGroup) + + wrapper := manual.NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}, + ) + scope := wrapper.Scopes()[0] + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := galleryName + shared.QuerySeparator + applicationName + sdpItems, err := searchable.Search(ctx, scope, searchQuery, true) + if err != nil { + t.Fatalf("Failed to search gallery application versions: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one gallery application version, got %d", len(sdpItems)) + } + + var found bool + expectedUniqueAttr := shared.CompositeLookupKey(galleryName, applicationName, versionName) + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == expectedUniqueAttr { + found = true + break + } + } + + if !found { + t.Fatalf("Expected to find gallery application version %s in the search results", versionName) + } + + log.Printf("Found %d gallery application versions in resource group %s", len(sdpItems), resourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for gallery application version %s", versionName) + + wrapper := manual.NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}, + ) + scope := wrapper.Scopes()[0] + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + query := shared.CompositeLookupKey(galleryName, applicationName, versionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGalleryApplicationVersion.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeGalleryApplicationVersion.String(), sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, resourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for gallery application version %s", versionName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for gallery application version %s", versionName) + + wrapper := manual.NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}, + ) + scope := wrapper.Scopes()[0] + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + query := shared.CompositeLookupKey(galleryName, applicationName, versionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for gallery application version %s", len(linkedQueries), versionName) + + // Should have at least Gallery and Gallery Application parent links + if len(linkedQueries) < 2 { + t.Fatalf("Expected at least 2 linked item queries (Gallery, Gallery Application), got %d", len(linkedQueries)) + } + + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + } + }) + }) +} diff --git a/sources/azure/integration-tests/compute-image_test.go b/sources/azure/integration-tests/compute-image_test.go index a9c57f99..78ceef2c 100644 --- a/sources/azure/integration-tests/compute-image_test.go +++ b/sources/azure/integration-tests/compute-image_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -274,32 +274,13 @@ func TestComputeImageIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - continue - } - - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - // Check if this is a link to the source disk if query.GetType() == azureshared.ComputeDisk.String() && query.GetQuery() == integrationTestImageDiskName { foundDiskLink = true - // Verify blast propagation for disk link - if !bp.GetIn() { - t.Error("Expected In=true for disk link (if source disk is deleted/modified, image becomes invalid)") - } - if bp.GetOut() { - t.Error("Expected Out=false for disk link (if image is deleted, source disk remains)") - } } - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) } // Verify we found the expected disk link diff --git a/sources/azure/integration-tests/compute-proximity-placement-group_test.go b/sources/azure/integration-tests/compute-proximity-placement-group_test.go index c2aae14a..7280cf75 100644 --- a/sources/azure/integration-tests/compute-proximity-placement-group_test.go +++ b/sources/azure/integration-tests/compute-proximity-placement-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -202,15 +202,6 @@ func TestComputeProximityPlacementGroupIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // PPG links use In: true, Out: true per adapter - if !bp.GetIn() || !bp.GetOut() { - t.Errorf("Expected BlastPropagation In=true Out=true for PPG links, got In=%v Out=%v", bp.GetIn(), bp.GetOut()) - } - } } }) diff --git a/sources/azure/integration-tests/compute-snapshot_test.go b/sources/azure/integration-tests/compute-snapshot_test.go new file mode 100644 index 00000000..c6388e86 --- /dev/null +++ b/sources/azure/integration-tests/compute-snapshot_test.go @@ -0,0 +1,429 @@ +package integrationtests + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + "k8s.io/utils/ptr" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +const ( + integrationTestSnapshotName = "ovm-integ-test-snapshot" + integrationTestDiskForSnapName = "ovm-integ-test-disk-for-snap" +) + +func TestComputeSnapshotIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + snapshotClient, err := armcompute.NewSnapshotsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Snapshots client: %v", err) + } + + diskClient, err := armcompute.NewDisksClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Disks client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Setup", func(t *testing.T) { + ctx := t.Context() + + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create resource group: %v", err) + } + + // Create a disk to snapshot from + err = createDisk(ctx, diskClient, integrationTestResourceGroup, integrationTestDiskForSnapName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create disk: %v", err) + } + + err = waitForDiskAvailable(ctx, diskClient, integrationTestResourceGroup, integrationTestDiskForSnapName) + if err != nil { + t.Fatalf("Failed waiting for disk to be available: %v", err) + } + + // Get disk ID for snapshot creation + diskResp, err := diskClient.Get(ctx, integrationTestResourceGroup, integrationTestDiskForSnapName, nil) + if err != nil { + t.Fatalf("Failed to get disk: %v", err) + } + + // Create snapshot from the disk + err = createSnapshot(ctx, snapshotClient, integrationTestResourceGroup, integrationTestSnapshotName, integrationTestLocation, *diskResp.ID) + if err != nil { + t.Fatalf("Failed to create snapshot: %v", err) + } + + err = waitForSnapshotAvailable(ctx, snapshotClient, integrationTestResourceGroup, integrationTestSnapshotName) + if err != nil { + t.Fatalf("Failed waiting for snapshot to be available: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + ctx := t.Context() + _, err := snapshotClient.Get(ctx, integrationTestResourceGroup, integrationTestSnapshotName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + t.Skipf("Snapshot %s does not exist - Setup may have failed. Skipping Run tests.", integrationTestSnapshotName) + } + } + + t.Run("GetSnapshot", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving snapshot %s in subscription %s, resource group %s", + integrationTestSnapshotName, subscriptionID, integrationTestResourceGroup) + + snapshotWrapper := manual.NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := snapshotWrapper.Scopes()[0] + + snapshotAdapter := sources.WrapperToAdapter(snapshotWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := snapshotAdapter.Get(ctx, scope, integrationTestSnapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + if uniqueAttrValue != integrationTestSnapshotName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestSnapshotName, uniqueAttrValue) + } + + if sdpItem.GetType() != azureshared.ComputeSnapshot.String() { + t.Fatalf("Expected type %s, got %s", azureshared.ComputeSnapshot, sdpItem.GetType()) + } + + log.Printf("Successfully retrieved snapshot %s", integrationTestSnapshotName) + }) + + t.Run("ListSnapshots", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing snapshots in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + snapshotWrapper := manual.NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := snapshotWrapper.Scopes()[0] + + snapshotAdapter := sources.WrapperToAdapter(snapshotWrapper, sdpcache.NewNoOpCache()) + + listable, ok := snapshotAdapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Failed to list snapshots: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one snapshot, got %d", len(sdpItems)) + } + + var found bool + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestSnapshotName { + found = true + if item.GetType() != azureshared.ComputeSnapshot.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeSnapshot, item.GetType()) + } + break + } + } + + if !found { + t.Fatalf("Expected to find snapshot %s in the list of snapshots", integrationTestSnapshotName) + } + + log.Printf("Found %d snapshots in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for snapshot %s", integrationTestSnapshotName) + + snapshotWrapper := manual.NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := snapshotWrapper.Scopes()[0] + + snapshotAdapter := sources.WrapperToAdapter(snapshotWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := snapshotAdapter.Get(ctx, scope, integrationTestSnapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeSnapshot.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeSnapshot, sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.GetHealth() != sdp.Health_HEALTH_OK { + t.Errorf("Expected health OK, got %s", sdpItem.GetHealth()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for snapshot %s", integrationTestSnapshotName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for snapshot %s", integrationTestSnapshotName) + + snapshotWrapper := manual.NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := snapshotWrapper.Scopes()[0] + + snapshotAdapter := sources.WrapperToAdapter(snapshotWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := snapshotAdapter.Get(ctx, scope, integrationTestSnapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for snapshot %s", len(linkedQueries), integrationTestSnapshotName) + + // The snapshot was created from a disk, so we expect a link to the source disk + var hasDiskLink bool + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + + if query.GetType() == azureshared.ComputeDisk.String() { + hasDiskLink = true + if query.GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected disk link method to be GET, got %s", query.GetMethod()) + } + if query.GetQuery() != integrationTestDiskForSnapName { + t.Errorf("Expected disk link query to be %s, got %s", integrationTestDiskForSnapName, query.GetQuery()) + } + } + + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) + } + + if !hasDiskLink { + t.Error("Expected to find a link to the source disk") + } + + log.Printf("Verified %d linked item queries for snapshot %s", len(linkedQueries), integrationTestSnapshotName) + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + // Delete snapshot first + err := deleteSnapshot(ctx, snapshotClient, integrationTestResourceGroup, integrationTestSnapshotName) + if err != nil { + t.Fatalf("Failed to delete snapshot: %v", err) + } + + // Delete the source disk + err = deleteDisk(ctx, diskClient, integrationTestResourceGroup, integrationTestDiskForSnapName) + if err != nil { + t.Fatalf("Failed to delete disk: %v", err) + } + }) +} + +// createSnapshot creates an Azure snapshot from a source disk (idempotent) +func createSnapshot(ctx context.Context, client *armcompute.SnapshotsClient, resourceGroupName, snapshotName, location, sourceDiskID string) error { + existingSnapshot, err := client.Get(ctx, resourceGroupName, snapshotName, nil) + if err == nil { + if existingSnapshot.Properties != nil && existingSnapshot.Properties.ProvisioningState != nil { + state := *existingSnapshot.Properties.ProvisioningState + if state == "Succeeded" { + log.Printf("Snapshot %s already exists with state %s, skipping creation", snapshotName, state) + return nil + } + log.Printf("Snapshot %s exists but in state %s, will wait for it", snapshotName, state) + } else { + log.Printf("Snapshot %s already exists, skipping creation", snapshotName) + return nil + } + } + + poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, snapshotName, armcompute.Snapshot{ + Location: ptr.To(location), + Properties: &armcompute.SnapshotProperties{ + CreationData: &armcompute.CreationData{ + CreateOption: ptr.To(armcompute.DiskCreateOptionCopy), + SourceResourceID: ptr.To(sourceDiskID), + }, + }, + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-snapshot"), + }, + }, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Snapshot %s already exists (conflict), skipping creation", snapshotName) + return nil + } + return fmt.Errorf("failed to begin creating snapshot: %w", err) + } + + resp, err := poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to create snapshot: %w", err) + } + + if resp.Properties == nil || resp.Properties.ProvisioningState == nil { + return fmt.Errorf("snapshot created but provisioning state is unknown") + } + + provisioningState := *resp.Properties.ProvisioningState + if provisioningState != "Succeeded" { + return fmt.Errorf("snapshot provisioning state is %s, expected Succeeded", provisioningState) + } + + log.Printf("Snapshot %s created successfully with provisioning state: %s", snapshotName, provisioningState) + return nil +} + +// waitForSnapshotAvailable polls until the snapshot is available via the Get API +func waitForSnapshotAvailable(ctx context.Context, client *armcompute.SnapshotsClient, resourceGroupName, snapshotName string) error { + maxAttempts := 20 + pollInterval := 5 * time.Second + + log.Printf("Waiting for snapshot %s to be available via API...", snapshotName) + + for attempt := 1; attempt <= maxAttempts; attempt++ { + resp, err := client.Get(ctx, resourceGroupName, snapshotName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Snapshot %s not yet available (attempt %d/%d), waiting %v...", snapshotName, attempt, maxAttempts, pollInterval) + time.Sleep(pollInterval) + continue + } + return fmt.Errorf("error checking snapshot availability: %w", err) + } + + if resp.Properties != nil && resp.Properties.ProvisioningState != nil { + state := *resp.Properties.ProvisioningState + if state == "Succeeded" { + log.Printf("Snapshot %s is available with provisioning state: %s", snapshotName, state) + return nil + } + if state == "Failed" { + return fmt.Errorf("snapshot provisioning failed with state: %s", state) + } + log.Printf("Snapshot %s provisioning state: %s (attempt %d/%d), waiting...", snapshotName, state, attempt, maxAttempts) + time.Sleep(pollInterval) + continue + } + + log.Printf("Snapshot %s is available", snapshotName) + return nil + } + + return fmt.Errorf("timeout waiting for snapshot %s to be available after %d attempts", snapshotName, maxAttempts) +} + +// deleteSnapshot deletes an Azure snapshot +func deleteSnapshot(ctx context.Context, client *armcompute.SnapshotsClient, resourceGroupName, snapshotName string) error { + poller, err := client.BeginDelete(ctx, resourceGroupName, snapshotName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Snapshot %s not found, skipping deletion", snapshotName) + return nil + } + return fmt.Errorf("failed to begin deleting snapshot: %w", err) + } + + _, err = poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to delete snapshot: %w", err) + } + + log.Printf("Snapshot %s deleted successfully", snapshotName) + return nil +} diff --git a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go index 8da2091a..1c89a6df 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -269,49 +269,14 @@ func TestComputeVirtualMachineExtensionIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestExtensionVMName { t.Errorf("Expected VM link query to be %s, got %s", integrationTestExtensionVMName, liq.GetQuery().GetQuery()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected VM blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected VM blast propagation Out=false, got true") - } case azureshared.KeyVaultVault.String(): // Key Vault links may be present if ProtectedSettingsFromKeyVault is set - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Key Vault blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Key Vault blast propagation Out=false, got true") - } case stdlib.NetworkHTTP.String(): // HTTP links may be present if settings contain URLs - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected HTTP blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected HTTP blast propagation Out=true, got false") - } case stdlib.NetworkDNS.String(): // DNS links may be present if settings contain DNS names - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected DNS blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected DNS blast propagation Out=true, got false") - } case stdlib.NetworkIP.String(): // IP links may be present if settings contain IP addresses - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected IP blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected IP blast propagation Out=true, got false") - } } } diff --git a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go index 67482ac1..e887ac32 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -269,49 +269,14 @@ func TestComputeVirtualMachineRunCommandIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestRunCommandVMName { t.Errorf("Expected VM link query to be %s, got %s", integrationTestRunCommandVMName, liq.GetQuery().GetQuery()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected VM blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected VM blast propagation Out=false, got true") - } case azureshared.StorageAccount.String(): // Storage account links may be present if outputBlobUri, errorBlobUri, or scriptUri are set - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Storage Account blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Storage Account blast propagation Out=false, got true") - } case azureshared.StorageBlobContainer.String(): // Blob container links may be present if outputBlobUri, errorBlobUri, or scriptUri are set - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Blob Container blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Blob Container blast propagation Out=false, got true") - } case stdlib.NetworkHTTP.String(): // HTTP links may be present if scriptUri is HTTP/HTTPS - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected HTTP blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected HTTP blast propagation Out=true, got false") - } case stdlib.NetworkDNS.String(): // DNS links may be present if scriptUri contains a DNS name - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected DNS blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected DNS blast propagation Out=true, got false") - } } } diff --git a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go index 0abf3148..f71515fc 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -229,13 +229,6 @@ func TestComputeVirtualMachineScaleSetIntegration(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected subnet link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected subnet blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected subnet blast propagation Out=false, got true") - } case azureshared.ComputeVirtualMachine.String(): hasVMLink = true // Verify VM link properties (VM instances are linked via SEARCH) @@ -245,26 +238,12 @@ func TestComputeVirtualMachineScaleSetIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestVMSSName { t.Errorf("Expected VM link query to be %s, got %s", integrationTestVMSSName, liq.GetQuery().GetQuery()) } - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected VM blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected VM blast propagation Out=true, got false") - } case azureshared.ComputeVirtualMachineExtension.String(): // Extensions may or may not be present depending on VMSS setup // Verify extension link properties if present if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected extension link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected extension blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected extension blast propagation Out=true, got false") - } } } diff --git a/sources/azure/integration-tests/compute-virtual-machine_test.go b/sources/azure/integration-tests/compute-virtual-machine_test.go index 5db54d03..2ef716e8 100644 --- a/sources/azure/integration-tests/compute-virtual-machine_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -239,26 +239,12 @@ func TestComputeVirtualMachineIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestVMName { t.Errorf("Expected run command link query to be %s, got %s", integrationTestVMName, liq.GetQuery().GetQuery()) } - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected run command blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected run command blast propagation Out=true, got false") - } case azureshared.ComputeVirtualMachineExtension.String(): // Extensions may or may not be present depending on VM setup // Verify extension link properties if present if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected extension link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected extension blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected extension blast propagation Out=true, got false") - } } } diff --git a/sources/azure/integration-tests/dbforpostgresql-database_test.go b/sources/azure/integration-tests/dbforpostgresql-database_test.go index a5704f46..e2efc7ed 100644 --- a/sources/azure/integration-tests/dbforpostgresql-database_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-database_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -236,18 +236,6 @@ func TestDBforPostgreSQLDatabaseIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Expected BlastPropagation to be set for PostgreSQL server link") - } else { - if !bp.GetIn() { - t.Error("Expected BlastPropagation.In to be true for PostgreSQL server link") - } - if bp.GetOut() { - t.Error("Expected BlastPropagation.Out to be false for PostgreSQL server link") - } - } break } } diff --git a/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go b/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go index b42ef5e5..d0a39a7f 100644 --- a/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -226,12 +226,6 @@ func TestDBforPostgreSQLFlexibleServerIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - } } // Check for conditional links @@ -253,63 +247,10 @@ func TestDBforPostgreSQLFlexibleServerIntegration(t *testing.T) { } } - log.Printf("Verified %d linked item queries for PostgreSQL Flexible Server %s (hasSubnet: %v, hasVNet: %v, hasDNS: %v)", - len(linkedQueries), postgreSQLServerName, hasSubnetLink, hasVirtualNetworkLink, hasDNSLink) - }) - - t.Run("VerifyChildResourceBlastPropagation", func(t *testing.T) { - ctx := t.Context() - - log.Printf("Verifying blast propagation for child resources of PostgreSQL Flexible Server %s", postgreSQLServerName) - - pgServerWrapper := manual.NewDBforPostgreSQLFlexibleServer( - clients.NewPostgreSQLFlexibleServersClient(postgreSQLServerClient), - []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, - ) - scope := pgServerWrapper.Scopes()[0] - - pgServerAdapter := sources.WrapperToAdapter(pgServerWrapper, sdpcache.NewNoOpCache()) - sdpItem, qErr := pgServerAdapter.Get(ctx, scope, postgreSQLServerName, true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - linkedQueries := sdpItem.GetLinkedItemQueries() - - // Verify specific blast propagation patterns - blastPropagationTests := map[string]struct { - in bool - out bool - }{ - // Child resources that depend on server (In: true, Out: false) - azureshared.DBforPostgreSQLDatabase.String(): {in: true, out: false}, - // Child resources that affect server connectivity/configuration (In: true, Out: true) - azureshared.DBforPostgreSQLFlexibleServerFirewallRule.String(): {in: true, out: true}, - azureshared.DBforPostgreSQLFlexibleServerConfiguration.String(): {in: true, out: true}, - } - - for _, liq := range linkedQueries { - linkedType := liq.GetQuery().GetType() - if expected, ok := blastPropagationTests[linkedType]; ok { - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - continue - } - - if bp.GetIn() != expected.in { - t.Errorf("Expected BlastPropagation.In=%v for %s, got %v", expected.in, linkedType, bp.GetIn()) - } - - if bp.GetOut() != expected.out { - t.Errorf("Expected BlastPropagation.Out=%v for %s, got %v", expected.out, linkedType, bp.GetOut()) - } - } - } - - log.Printf("Verified blast propagation for all child resources") - }) + log.Printf("Verified %d linked item queries for PostgreSQL Flexible Server %s (hasSubnet: %v, hasVNet: %v, hasDNS: %v)", + len(linkedQueries), postgreSQLServerName, hasSubnetLink, hasVirtualNetworkLink, hasDNSLink) }) +}) t.Run("Teardown", func(t *testing.T) { ctx := t.Context() diff --git a/sources/azure/integration-tests/documentdb-database-accounts_test.go b/sources/azure/integration-tests/documentdb-database-accounts_test.go index 88d581de..6d59dbb4 100644 --- a/sources/azure/integration-tests/documentdb-database-accounts_test.go +++ b/sources/azure/integration-tests/documentdb-database-accounts_test.go @@ -15,8 +15,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/keyvault-managed-hsm_test.go b/sources/azure/integration-tests/keyvault-managed-hsm_test.go index 200e4b5d..beeb2e54 100644 --- a/sources/azure/integration-tests/keyvault-managed-hsm_test.go +++ b/sources/azure/integration-tests/keyvault-managed-hsm_test.go @@ -14,9 +14,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -286,16 +286,6 @@ func TestKeyVaultManagedHSMIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } } log.Printf("Verified %d linked item queries for Managed HSM %s", len(linkedQueries), integrationTestManagedHSMName) diff --git a/sources/azure/integration-tests/keyvault-secret_test.go b/sources/azure/integration-tests/keyvault-secret_test.go index 4f4939e9..bfca030b 100644 --- a/sources/azure/integration-tests/keyvault-secret_test.go +++ b/sources/azure/integration-tests/keyvault-secret_test.go @@ -14,8 +14,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -275,12 +275,6 @@ func TestKeyVaultSecretIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != vaultName { t.Errorf("Expected linked query to Key Vault %s, got %s", vaultName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/integration-tests/keyvault-vault_test.go b/sources/azure/integration-tests/keyvault-vault_test.go index 29d9d390..187756c2 100644 --- a/sources/azure/integration-tests/keyvault-vault_test.go +++ b/sources/azure/integration-tests/keyvault-vault_test.go @@ -15,8 +15,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go index 3d1e32e2..a622263a 100644 --- a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -212,13 +212,6 @@ func TestManagedIdentityUserAssignedIdentityIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected federated credential link scope to be %s, got %s", scope, liq.GetQuery().GetScope()) } - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected federated credential blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected federated credential blast propagation Out=true, got false") - } default: t.Errorf("Unexpected linked item type: %s", liq.GetQuery().GetType()) } diff --git a/sources/azure/integration-tests/network-application-gateway_test.go b/sources/azure/integration-tests/network-application-gateway_test.go index a06655fa..7525eee2 100644 --- a/sources/azure/integration-tests/network-application-gateway_test.go +++ b/sources/azure/integration-tests/network-application-gateway_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -318,16 +318,6 @@ func TestNetworkApplicationGatewayIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } } // Verify critical linked types were found diff --git a/sources/azure/integration-tests/network-load-balancer_test.go b/sources/azure/integration-tests/network-load-balancer_test.go index eb3aae5e..b6cd87ba 100644 --- a/sources/azure/integration-tests/network-load-balancer_test.go +++ b/sources/azure/integration-tests/network-load-balancer_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -280,27 +280,6 @@ func TestNetworkLoadBalancerIntegration(t *testing.T) { linkedType := liq.GetQuery().GetType() if _, exists := expectedLinkedTypes[linkedType]; exists { expectedLinkedTypes[linkedType] = true - - if liq.GetBlastPropagation() == nil { - t.Errorf("Expected blast propagation to be set for linked type %s", linkedType) - } else { - switch linkedType { - case azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(): - if liq.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected FrontendIPConfiguration blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected FrontendIPConfiguration blast propagation Out=true, got false") - } - case azureshared.NetworkPublicIPAddress.String(): - if liq.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected PublicIPAddress blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Errorf("Expected PublicIPAddress blast propagation Out=false, got true") - } - } - } } } @@ -336,27 +315,6 @@ func TestNetworkLoadBalancerIntegration(t *testing.T) { linkedType := liq.GetQuery().GetType() if _, exists := expectedLinkedTypes[linkedType]; exists { expectedLinkedTypes[linkedType] = true - - if liq.GetBlastPropagation() == nil { - t.Errorf("Expected blast propagation to be set for linked type %s", linkedType) - } else { - switch linkedType { - case azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(): - if liq.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected FrontendIPConfiguration blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected FrontendIPConfiguration blast propagation Out=true, got false") - } - case azureshared.NetworkSubnet.String(): - if liq.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected Subnet blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Errorf("Expected Subnet blast propagation Out=false, got true") - } - } - } } } diff --git a/sources/azure/integration-tests/network-network-interface_test.go b/sources/azure/integration-tests/network-network-interface_test.go index 828b0049..a496409b 100644 --- a/sources/azure/integration-tests/network-network-interface_test.go +++ b/sources/azure/integration-tests/network-network-interface_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -209,31 +209,10 @@ func TestNetworkNetworkInterfaceIntegration(t *testing.T) { switch liq.GetQuery().GetType() { case azureshared.NetworkNetworkInterfaceIPConfiguration.String(): hasIPConfigLink = true - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected IP config blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected IP config blast propagation Out=true, got false") - } case azureshared.ComputeVirtualMachine.String(): // VM link may or may not be present depending on whether NIC is attached - // Verify blast propagation if present (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected VM blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected VM blast propagation Out=true, got false") - } case azureshared.NetworkNetworkSecurityGroup.String(): // NSG link may or may not be present - // Verify blast propagation if present (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected NSG blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected NSG blast propagation Out=false, got true") - } } } diff --git a/sources/azure/integration-tests/network-network-security-group_test.go b/sources/azure/integration-tests/network-network-security-group_test.go index a38c54bf..4618167b 100644 --- a/sources/azure/integration-tests/network-network-security-group_test.go +++ b/sources/azure/integration-tests/network-network-security-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -242,19 +242,8 @@ func TestNetworkNetworkSecurityGroupIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } - - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) } // Verify that default security rules are linked (they should always exist) @@ -262,14 +251,6 @@ func TestNetworkNetworkSecurityGroupIntegration(t *testing.T) { for _, liq := range linkedQueries { if liq.GetQuery().GetType() == azureshared.NetworkDefaultSecurityRule.String() { hasDefaultSecurityRuleLink = true - // Verify blast propagation for default security rules - bp := liq.GetBlastPropagation() - if bp.GetIn() != true { - t.Error("Expected default security rule blast propagation In=true, got false") - } - if bp.GetOut() != false { - t.Error("Expected default security rule blast propagation Out=false, got true") - } break } } @@ -282,14 +263,6 @@ func TestNetworkNetworkSecurityGroupIntegration(t *testing.T) { for _, liq := range linkedQueries { if liq.GetQuery().GetType() == azureshared.NetworkSecurityRule.String() { hasSecurityRuleLink = true - // Verify blast propagation for security rules - bp := liq.GetBlastPropagation() - if bp.GetIn() != true { - t.Error("Expected security rule blast propagation In=true, got false") - } - if bp.GetOut() != false { - t.Error("Expected security rule blast propagation Out=false, got true") - } // Verify the query contains the NSG name and rule name query := liq.GetQuery().GetQuery() if query == "" { diff --git a/sources/azure/integration-tests/network-public-ip-address_test.go b/sources/azure/integration-tests/network-public-ip-address_test.go index e68169eb..9fea6d49 100644 --- a/sources/azure/integration-tests/network-public-ip-address_test.go +++ b/sources/azure/integration-tests/network-public-ip-address_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -250,18 +250,6 @@ func TestNetworkPublicIPAddressIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Expected BlastPropagation to be set for network interface link") - } else { - if !bp.GetIn() { - t.Error("Expected BlastPropagation.In to be true for network interface link") - } - if bp.GetOut() { - t.Error("Expected BlastPropagation.Out to be false for network interface link") - } - } break } } diff --git a/sources/azure/integration-tests/network-route-table_test.go b/sources/azure/integration-tests/network-route-table_test.go index c2d2a8b9..1de639d7 100644 --- a/sources/azure/integration-tests/network-route-table_test.go +++ b/sources/azure/integration-tests/network-route-table_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -259,35 +259,15 @@ func TestNetworkRouteTableIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } - - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) } // Verify that routes are linked (we created one named integrationTestRouteName) var hasRouteLink bool for _, liq := range linkedQueries { if liq.GetQuery().GetType() == azureshared.NetworkRoute.String() { - hasRouteLink = true - // Verify blast propagation for routes - bp := liq.GetBlastPropagation() - if bp.GetIn() != true { - t.Error("Expected route blast propagation In=true, got false") - } - if bp.GetOut() != false { - t.Error("Expected route blast propagation Out=false, got true") - } - // Verify the query contains the route table name and route name + hasRouteLink = true // Verify the query contains the route table name and route name query := liq.GetQuery().GetQuery() if query == "" { t.Error("Expected route query to be non-empty") diff --git a/sources/azure/integration-tests/network-virtual-network_test.go b/sources/azure/integration-tests/network-virtual-network_test.go index 2c0e0e43..9a831b64 100644 --- a/sources/azure/integration-tests/network-virtual-network_test.go +++ b/sources/azure/integration-tests/network-virtual-network_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -185,12 +185,6 @@ func TestNetworkVirtualNetworkIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestVNetName { t.Errorf("Expected subnet link query to be %s, got %s", integrationTestVNetName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected subnet blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected subnet blast propagation Out=true, got false") - } break } } @@ -209,12 +203,6 @@ func TestNetworkVirtualNetworkIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestVNetName { t.Errorf("Expected peering link query to be %s, got %s", integrationTestVNetName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected peering blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected peering blast propagation Out=true, got false") - } break } } diff --git a/sources/azure/integration-tests/network-zone_test.go b/sources/azure/integration-tests/network-zone_test.go index ec54ad17..cd2e363d 100644 --- a/sources/azure/integration-tests/network-zone_test.go +++ b/sources/azure/integration-tests/network-zone_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -272,19 +272,6 @@ func TestNetworkZoneIntegration(t *testing.T) { if linkedScope != scope { t.Errorf("Expected linked query scope %s, got %s", scope, linkedScope) } - - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - } else { - if bp.GetIn() != true { - t.Errorf("Expected BlastPropagation.In=true for %s, got false", linkedType) - } - if bp.GetOut() != true { - t.Errorf("Expected BlastPropagation.Out=true for %s, got false", linkedType) - } - } } // Verify DNS name server links (standard library) @@ -297,19 +284,6 @@ func TestNetworkZoneIntegration(t *testing.T) { if linkedScope != "global" { t.Errorf("Expected linked query scope 'global' for DNS name server, got %s", linkedScope) } - - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for DNS name server") - } else { - if bp.GetIn() != true { - t.Errorf("Expected BlastPropagation.In=true for DNS name server, got false") - } - if bp.GetOut() != true { - t.Errorf("Expected BlastPropagation.Out=true for DNS name server, got false") - } - } } // Verify Virtual Network links (if present) @@ -317,19 +291,6 @@ func TestNetworkZoneIntegration(t *testing.T) { if method != sdp.QueryMethod_GET { t.Errorf("Expected linked query method GET for Virtual Network, got %s", method) } - - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for Virtual Network") - } else { - if bp.GetIn() != true { - t.Errorf("Expected BlastPropagation.In=true for Virtual Network, got false") - } - if bp.GetOut() != false { - t.Errorf("Expected BlastPropagation.Out=false for Virtual Network, got true") - } - } } } diff --git a/sources/azure/integration-tests/sql-database_test.go b/sources/azure/integration-tests/sql-database_test.go index 2646d385..be46d5a0 100644 --- a/sources/azure/integration-tests/sql-database_test.go +++ b/sources/azure/integration-tests/sql-database_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -236,18 +236,6 @@ func TestSQLDatabaseIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Expected BlastPropagation to be set for SQL server link") - } else { - if !bp.GetIn() { - t.Error("Expected BlastPropagation.In to be true for SQL server link") - } - if bp.GetOut() { - t.Error("Expected BlastPropagation.Out to be false for SQL server link") - } - } break } } diff --git a/sources/azure/integration-tests/sql-server_test.go b/sources/azure/integration-tests/sql-server_test.go index 76a625cb..f670a2f1 100644 --- a/sources/azure/integration-tests/sql-server_test.go +++ b/sources/azure/integration-tests/sql-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -239,12 +239,6 @@ func TestSQLServerIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - } } } @@ -255,85 +249,9 @@ func TestSQLServerIntegration(t *testing.T) { } } - log.Printf("Verified %d linked item queries for SQL server %s", len(linkedQueries), sqlServerName) - }) - - t.Run("VerifyChildResourceBlastPropagation", func(t *testing.T) { - ctx := t.Context() - - log.Printf("Verifying blast propagation for child resources of SQL server %s", sqlServerName) - - sqlServerWrapper := manual.NewSqlServer( - clients.NewSqlServersClient(sqlServerClient), - []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, - ) - scope := sqlServerWrapper.Scopes()[0] - - sqlServerAdapter := sources.WrapperToAdapter(sqlServerWrapper, sdpcache.NewNoOpCache()) - sdpItem, qErr := sqlServerAdapter.Get(ctx, scope, sqlServerName, true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - linkedQueries := sdpItem.GetLinkedItemQueries() - - // Verify specific blast propagation patterns - blastPropagationTests := map[string]struct { - in bool - out bool - }{ - // Child resources that depend on server (In: true, Out: false) - azureshared.SQLDatabase.String(): {in: true, out: false}, - azureshared.SQLElasticPool.String(): {in: true, out: false}, - azureshared.SQLServerSyncGroup.String(): {in: true, out: false}, - azureshared.SQLServerSyncAgent.String(): {in: true, out: false}, - azureshared.SQLServerUsage.String(): {in: true, out: false}, - azureshared.SQLServerOperation.String(): {in: true, out: false}, - azureshared.SQLServerAdvisor.String(): {in: true, out: false}, - azureshared.SQLServerPrivateLinkResource.String(): {in: true, out: false}, - // Child resources that affect server connectivity/security (In: true, Out: true) - azureshared.SQLServerFirewallRule.String(): {in: true, out: true}, - azureshared.SQLServerVirtualNetworkRule.String(): {in: true, out: true}, - azureshared.SQLServerKey.String(): {in: true, out: true}, - azureshared.SQLServerFailoverGroup.String(): {in: true, out: true}, - azureshared.SQLServerAdministrator.String(): {in: true, out: true}, - azureshared.SQLServerPrivateEndpointConnection.String(): {in: true, out: true}, - azureshared.SQLServerAuditingSetting.String(): {in: true, out: true}, - azureshared.SQLServerSecurityAlertPolicy.String(): {in: true, out: true}, - azureshared.SQLServerVulnerabilityAssessment.String(): {in: true, out: true}, - azureshared.SQLServerEncryptionProtector.String(): {in: true, out: true}, - azureshared.SQLServerBlobAuditingPolicy.String(): {in: true, out: true}, - azureshared.SQLServerAutomaticTuning.String(): {in: true, out: true}, - azureshared.SQLServerAdvancedThreatProtectionSetting.String(): {in: true, out: true}, - azureshared.SQLServerDnsAlias.String(): {in: true, out: true}, - azureshared.SQLServerBackupLongTermRetentionPolicy.String(): {in: true, out: true}, - azureshared.SQLServerDevOpsAuditSetting.String(): {in: true, out: true}, - azureshared.SQLServerTrustGroup.String(): {in: true, out: true}, - azureshared.SQLServerOutboundFirewallRule.String(): {in: true, out: true}, - } - - for _, liq := range linkedQueries { - linkedType := liq.GetQuery().GetType() - if expected, ok := blastPropagationTests[linkedType]; ok { - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - continue - } - - if bp.GetIn() != expected.in { - t.Errorf("Expected BlastPropagation.In=%v for %s, got %v", expected.in, linkedType, bp.GetIn()) - } - - if bp.GetOut() != expected.out { - t.Errorf("Expected BlastPropagation.Out=%v for %s, got %v", expected.out, linkedType, bp.GetOut()) - } - } - } - - log.Printf("Verified blast propagation for all child resources") - }) + log.Printf("Verified %d linked item queries for SQL server %s", len(linkedQueries), sqlServerName) }) +}) t.Run("Teardown", func(t *testing.T) { ctx := t.Context() diff --git a/sources/azure/integration-tests/storage-account_test.go b/sources/azure/integration-tests/storage-account_test.go index eb04c4cb..be3e69c9 100644 --- a/sources/azure/integration-tests/storage-account_test.go +++ b/sources/azure/integration-tests/storage-account_test.go @@ -8,9 +8,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -200,18 +200,6 @@ func TestStorageAccountIntegration(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { t.Errorf("Expected linked query method to be SEARCH, got %s", liq.GetQuery().GetMethod()) } - - // Verify blast propagation (parent to child: In=false, Out=true) - if liq.GetBlastPropagation() == nil { - t.Errorf("Expected blast propagation to be set for linked type %s", linkedType) - } else { - if liq.GetBlastPropagation().GetIn() != false { - t.Errorf("Expected blast propagation In=false for linked type %s, got true", linkedType) - } - if liq.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected blast propagation Out=true for linked type %s, got false", linkedType) - } - } } } diff --git a/sources/azure/integration-tests/storage-blob-container_test.go b/sources/azure/integration-tests/storage-blob-container_test.go index 8558917b..0ff9b17d 100644 --- a/sources/azure/integration-tests/storage-blob-container_test.go +++ b/sources/azure/integration-tests/storage-blob-container_test.go @@ -17,8 +17,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-fileshare_test.go b/sources/azure/integration-tests/storage-fileshare_test.go index 0d2ccd6a..d9a5a992 100644 --- a/sources/azure/integration-tests/storage-fileshare_test.go +++ b/sources/azure/integration-tests/storage-fileshare_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-queues_test.go b/sources/azure/integration-tests/storage-queues_test.go index 0275ae33..24dc4d14 100644 --- a/sources/azure/integration-tests/storage-queues_test.go +++ b/sources/azure/integration-tests/storage-queues_test.go @@ -13,8 +13,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -242,12 +242,6 @@ func TestStorageQueuesIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query to storage account %s, got %s", storageAccountName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/integration-tests/storage-table_test.go b/sources/azure/integration-tests/storage-table_test.go index 51e7954b..2a0bae7e 100644 --- a/sources/azure/integration-tests/storage-table_test.go +++ b/sources/azure/integration-tests/storage-table_test.go @@ -13,8 +13,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -242,12 +242,6 @@ func TestStorageTableIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query to storage account %s, got %s", storageAccountName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc b/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc deleted file mode 100644 index 6cb89c24..00000000 --- a/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc +++ /dev/null @@ -1,805 +0,0 @@ ---- -description: "Azure Manual Adapter development patterns and standards" -globs: **/*.go -alwaysApply: false ---- - -# Azure Manual Adapter Creation Rules - -This document provides comprehensive rules and patterns for creating Azure manual adapters in the Overmind platform. Follow these guidelines to ensure consistency, maintainability, and proper integration with the SDP (State Description Protocol) framework. - -## Table of Contents - -1. [Adapter Structure and Naming](#adapter-structure-and-naming) -2. [Wrapper Type Selection](#wrapper-type-selection) -3. [Base Struct Selection](#base-struct-selection) -4. [Required Methods Implementation](#required-methods-implementation) -5. [Terraform Mappings](#terraform-mappings) -6. [Get and Search Lookups](#get-and-search-lookups) -7. [Linked Item Queries](#linked-item-queries) -8. [Error Handling](#error-handling) -9. [Testing Patterns](#testing-patterns) -10. [Client Interface Patterns](#client-interface-patterns) -11. [Common Gotchas and Best Practices](#common-gotchas-and-best-practices) - -## Adapter Structure and Naming - -### File Naming Convention - -- Use kebab-case for file names: `compute-instance.go`, `big-query-table.go`, `cloud-kms-crypto-key.go` -- Test files should follow the same pattern with `_test.go` suffix: `compute-instance_test.go` - -### Package and Import Structure - -```go -package manual - -import ( - "context" - "fmt" // if using string formatting - "strings" // if parsing paths or URLs - - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute" // Use specific Azure SDK imports - - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/sources" - azureshared "github.com/overmindtech/cli/sources/azure/shared" - "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sources/stdlib" // Only if linking to stdlib resources -) -``` - -### Struct Naming - -- Wrapper struct: `{resourceName}Wrapper` (e.g., `computeInstanceWrapper`, `bigQueryTableWrapper`) -- Use camelCase with first letter lowercase for private structs -- Constructor function: `New{ResourceName}` (e.g., `NewComputeInstance`, `NewBigQueryTable`) - -## Wrapper Type Selection - -Choose the appropriate wrapper interface based on Azure API capabilities: - -### `Wrapper` (GET only) - -Use when the Azure API only supports individual resource retrieval: - -- Resources without list endpoints -- Resources that require specific identifiers for retrieval - -### `ListableWrapper` (GET + LIST) - -Use when the Azure API supports listing all resources in a scope: - -- Compute Virtual Machines (per resource group) -- Compute Disks (per resource group) -- Network Interfaces (per resource group) -- Resources scoped to resource groups or subscriptions - -### `SearchableWrapper` (GET + SEARCH) - -Use when the Azure API supports filtering/searching resources: - -- Resources that can be queried with filters -- Resources that support tag-based or property-based searches - -### `SearchableListableWrapper` (GET + LIST + SEARCH) - -Use when the Azure API supports both listing and searching: - -- Currently not used in existing adapters, but available for complex resources - -## Base Struct Selection - -Choose the appropriate base struct based on Azure resource scope: - -### `MultiResourceGroupBase` - Resource Group Scoped Resources (multi-scope) - -Resource-group-scoped adapters use **one adapter per resource type** that holds a slice of resource-group scopes. The engine calls List/Get once per scope; scope is resolved in each method via `ResourceGroupScopeFromScope(scope)`. - -```go -type computeVirtualMachineWrapper struct { - client clients.VirtualMachinesClient - *azureshared.MultiResourceGroupBase -} - -func NewComputeVirtualMachine(client clients.VirtualMachinesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { - return &computeVirtualMachineWrapper{ - client: client, - MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( - resourceGroupScopes, - sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - azureshared.ComputeVirtualMachine, - ), - } -} -``` - -In Get/List/ListStream/Search, resolve scope to resource group (and subscription when needed) with: - -```go -rgScope, err := c.ResourceGroupScopeFromScope(scope) -if err != nil { - return nil, azureshared.QueryError(err, scope, c.Type()) -} -// use rgScope.ResourceGroup and rgScope.SubscriptionID -``` - -**Examples:** Compute Virtual Machines, Compute Disks, Network Interfaces, Network Security Groups - -### `SubscriptionBase` - Subscription-Level Resources - -For resources scoped to the entire subscription: - -```go -type subscriptionWrapper struct { - client clients.SubscriptionClient - *azureshared.SubscriptionBase -} - -func NewSubscription(client clients.SubscriptionClient, subscriptionID string) sources.ListableWrapper { - return &subscriptionWrapper{ - client: client, - SubscriptionBase: azureshared.NewSubscriptionBase( - subscriptionID, - sdp.AdapterCategory_ADAPTER_CATEGORY_GENERAL, - azureshared.Subscription, - ), - } -} -``` - -**Examples:** Subscriptions, Resource Groups, Management Groups - -## Required Methods Implementation - -### IAM Permissions - -Always implement with specific Azure RBAC permissions: - -```go -func (c computeVirtualMachineWrapper) IAMPermissions() []string { - return []string{ - "Microsoft.Compute/virtualMachines/read", - } -} -``` - -### Predefined Role - -Always implement with the most restrictive Azure built-in role: - -```go -func (c computeVirtualMachineWrapper) PredefinedRole() string { - return "Reader" // or more specific role like "Virtual Machine Contributor" if write access needed -} -``` - -### Potential Links - -Document all possible linked resources: - -```go -func (c computeVirtualMachineWrapper) PotentialLinks() map[shared.ItemType]bool { - return shared.NewItemTypesSet( - stdlib.NetworkIP, - azureshared.ComputeDisk, - azureshared.NetworkNetworkInterface, - azureshared.NetworkVirtualNetwork, - azureshared.NetworkSubnet, - azureshared.ComputeAvailabilitySet, - ) -} -``` - -## Terraform Mappings - -### GET Method (Direct ID Match) - -Use when Terraform resource has a unique identifier that directly matches your adapter's unique attribute: - -```go -func (c computeVirtualMachineWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_GET, - // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_machine - TerraformQueryMap: "azurerm_virtual_machine.name", - }, - { - TerraformMethod: sdp.QueryMethod_GET, - // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_virtual_machine - TerraformQueryMap: "azurerm_linux_virtual_machine.name", - }, - { - TerraformMethod: sdp.QueryMethod_GET, - // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/windows_virtual_machine - TerraformQueryMap: "azurerm_windows_virtual_machine.name", - }, - } -} -``` - -### SEARCH Method (Multiple Parameters) - -Use when Terraform resource requires multiple parameters or different ID format: - -```go -func (b resourceWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_SEARCH, - // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/example - // Terraform uses: /subscriptions/{{subscription}}/resourceGroups/{{resourceGroup}}/providers/.../{{name}} - // Our adapter uses: resourceGroup + name as separate parameters - TerraformQueryMap: "azurerm_example.id", - }, - } -} -``` - -### Multiple Terraform Mappings - -For resources that can be referenced in multiple ways: - -```go -func (c resourceWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_GET, - TerraformQueryMap: "azurerm_resource.name", - }, - { - TerraformMethod: sdp.QueryMethod_GET, - TerraformQueryMap: "azurerm_resource.resource_id", - }, - } -} -``` - -## Get and Search Lookups - -### Single Key Lookup - -For resources with a single unique identifier: - -```go -var ComputeVirtualMachineLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeVirtualMachine) - -func (c computeVirtualMachineWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - ComputeVirtualMachineLookupByName, - } -} -``` - -### Multiple Keys Lookup (Order Matters) - -The order of lookups determines the order of `queryParts` in the `Get` method: - -```go -var ( - ResourceGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ResourceGroup) - VirtualNetworkLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkVirtualNetwork) -) - -func (b subnetWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - ResourceGroupLookupByName, // First key: resource group (queryParts[0]) - VirtualNetworkLookupByName, // Second key: virtual network (queryParts[1]) - SubnetLookupByName, // Third key: subnet (queryParts[2]) - } -} - -func (b subnetWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - resourceGroup := queryParts[0] // From ResourceGroupLookupByName - vnetName := queryParts[1] // From VirtualNetworkLookupByName - subnetName := queryParts[2] // From SubnetLookupByName - // ... implementation -} -``` - -### Multiple Keys with Composite Lookup - -For complex hierarchical resources: - -```go -var ( - SubscriptionLookupByID = shared.NewItemTypeLookup("id", azureshared.Subscription) - ResourceGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ResourceGroup) - ResourceLookupByName = shared.NewItemTypeLookup("name", azureshared.Resource) -) - -func (c resourceWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - SubscriptionLookupByID, // First key: subscription ID - ResourceGroupLookupByName, // Second key: resource group name - ResourceLookupByName, // Third key: resource name - } -} -``` - -### Search Lookups - -Define search parameters for SearchableWrapper: - -```go -// Single search lookup -func (b resourceWrapper) SearchLookups() []sources.ItemTypeLookups { - return []sources.ItemTypeLookups{ - { - ResourceGroupLookupByName, // Search within a specific resource group - }, - } -} - -// Multiple search lookups (different search patterns) -func (c resourceWrapper) SearchLookups() []sources.ItemTypeLookups { - return []sources.ItemTypeLookups{ - { - SubscriptionLookupByID, // Search by subscription only - }, - { - SubscriptionLookupByID, // Search within specific resource group - ResourceGroupLookupByName, - }, - } -} -``` - -## Linked Item Queries - -### Basic Pattern - -Always use this pattern for creating linked item queries: - -```go -sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.TargetResourceType.String(), - Method: sdp.QueryMethod_GET, // or SEARCH - Query: "resource-identifier", - Scope: "appropriate-scope", - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // This resource affected if linked resource changes - Out: true, // Linked resource affected if this resource changes - }, -}) -``` - -### Resource ID Extraction - -Use helper functions for extracting parameters from Azure resource IDs: - -```go -// Extract resource name from Azure resource ID -// ID: /subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/disks/{diskName} -diskName := azureshared.ExtractResourceName(*vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID) -``` - -### Composite Lookup Keys - -Use for multi-parameter queries: - -```go -Query: shared.CompositeLookupKey(resourceGroup, resourceName) -``` - -### Blast Propagation Rules - -- **In: true, Out: true** - Tightly coupled resources (parent-child relationships) -- **In: true, Out: false** - This resource depends on linked resource (e.g., disk depends on image) -- **In: false, Out: true** - Linked resource depends on this resource (e.g., instance depends on disk) - -### Common Linked Resource Patterns - -#### Network Resources - -```go -// Extract network interface name from Azure resource ID -if vm.Properties.NetworkProfile != nil && len(vm.Properties.NetworkProfile.NetworkInterfaces) > 0 { - for _, nicRef := range vm.Properties.NetworkProfile.NetworkInterfaces { - if nicRef.ID != nil { - nicName := azureshared.ExtractResourceName(*nicRef.ID) - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.NetworkNetworkInterface.String(), - Method: sdp.QueryMethod_GET, - Query: nicName, - Scope: scope, // Network interfaces are scoped to resource group - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } - } -} -``` - -#### Disk Resources - -```go -// Extract disk name from Azure resource ID -if vm.Properties.StorageProfile != nil && vm.Properties.StorageProfile.OSDisk != nil { - if vm.Properties.StorageProfile.OSDisk.ManagedDisk != nil && vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID != nil { - diskName := azureshared.ExtractResourceName(*vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID) - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.ComputeDisk.String(), - Method: sdp.QueryMethod_GET, - Query: diskName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } -} -``` - -## Error Handling - -### Standard Error Pattern - -Always use source-specific error wrapping: - -```go -if err != nil { - return nil, azureshared.QueryError(err, scope, c.Type()) -} -``` - -### Pager Pattern - -Handle Azure SDK pagers consistently. For multi-scope resource group adapters, resolve scope first: - -```go -rgScope, err := c.ResourceGroupScopeFromScope(scope) -if err != nil { - return nil, azureshared.QueryError(err, scope, c.Type()) -} -pager := c.client.NewListPager(rgScope.ResourceGroup, nil) -for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, azureshared.QueryError(err, scope, c.Type()) - } - for _, item := range page.Value { - // Process item... - } -} -``` - -### Stream Error Handling - -For streaming operations: - -```go -if err != nil { - stream.SendError(azureshared.QueryError(err, scope, c.Type())) - return -} - -// For item processing errors, continue with next item -if sdpErr != nil { - stream.SendError(sdpErr) - continue -} -``` - -## Testing Patterns - -### Test Structure - -Follow this exact pattern for all adapter tests: - -```go -func TestComputeVirtualMachine(t *testing.T) { - ctx := context.Background() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := mocks.NewMockVirtualMachinesClient(ctrl) - subscriptionID := "test-subscription-id" - resourceGroup := "test-resource-group" - - t.Run("Get", func(t *testing.T) { - wrapper := manual.NewComputeVirtualMachine(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) - vm := createAzureVirtualMachine("test-vm", "Succeeded") - mockClient.EXPECT().Get(ctx, resourceGroup, "test-vm", nil).Return(armcompute.VirtualMachinesClientGetResponse{ - VirtualMachine: *vm, - }, nil) - - adapter := sources.WrapperToAdapter(wrapper) - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-vm", true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - { - ExpectedType: azureshared.ComputeDisk.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-disk", - ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - // ... more test cases - } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) - }) - - t.Run("List", func(t *testing.T) { - // Test list functionality - }) - - t.Run("Search", func(t *testing.T) { - // Test search functionality (if applicable) - }) -} -``` - -### Required Test Cases - -- **Get method**: Test successful retrieval and error cases -- **StaticTests**: Test linked item queries using `shared.RunStaticTests` -- **HealthCheck**: Test health status mapping (if applicable) -- **List method**: Test the List method (for ListableWrapper) -- **Search method**: Test the Search method (for SearchableWrapper) -- **Interface compliance**: Verify adapter implements correct interfaces - -### Test Helper Functions - -Create helper functions for test data: - -```go -func createAzureVirtualMachine(vmName string, provisioningState string) *armcompute.VirtualMachine { - return &armcompute.VirtualMachine{ - Name: ptr.To(vmName), - Tags: map[string]*string{ - "env": ptr.To("test"), - }, - Properties: &armcompute.VirtualMachineProperties{ - ProvisioningState: ptr.To(provisioningState), - // ... other fields - }, - } -} -``` - -### Mock Expectations - -Use gomock for client mocking: - -```go -mockClient := mocks.NewMockVirtualMachinesClient(ctrl) -mockClient.EXPECT().Get(ctx, resourceGroup, vmName, nil).Return(expectedResult, nil) -``` - -## Client Interface Patterns - -### Typed Client Interfaces - -Define typed client interfaces in the clients package: - -```go -// In sources/azure/clients/virtual-machines-client.go -type VirtualMachinesClient interface { - Get(ctx context.Context, resourceGroupName string, vmName string, options *armcompute.VirtualMachinesClientGetOptions) (armcompute.VirtualMachinesClientGetResponse, error) - NewListPager(resourceGroupName string, options *armcompute.VirtualMachinesClientListOptions) *runtime.Pager[armcompute.VirtualMachinesClientListResponse] -} - -// Constructor function -func NewVirtualMachinesClient(client *armcompute.VirtualMachinesClient) VirtualMachinesClient { - return &virtualMachinesClientAdapter{client: client} -} -``` - -### Client Implementation - -Implement the interface with proper error handling: - -```go -type virtualMachinesClientAdapter struct { - client *armcompute.VirtualMachinesClient -} - -func (c *virtualMachinesClientAdapter) Get(ctx context.Context, resourceGroupName string, vmName string, options *armcompute.VirtualMachinesClientGetOptions) (armcompute.VirtualMachinesClientGetResponse, error) { - return c.client.Get(ctx, resourceGroupName, vmName, options) -} - -func (c *virtualMachinesClientAdapter) NewListPager(resourceGroupName string, options *armcompute.VirtualMachinesClientListOptions) *runtime.Pager[armcompute.VirtualMachinesClientListResponse] { - return c.client.NewListPager(resourceGroupName, options) -} -``` - -## Common Gotchas and Best Practices - -### 1. Unique Attribute Consistency - -Ensure the `UniqueAttribute` in the SDP item matches the lookup key: - -```go -// If using composite lookup key -err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(values...)) -sdpItem := &sdp.Item{ - Type: azureshared.ComputeVirtualMachine.String(), - UniqueAttribute: "uniqueAttr", // Must match the attribute name above - // ... -} -``` - -### 2. Scope Handling - -Use appropriate scope helper functions: - -```go -// For resource group scoped resources -scope := fmt.Sprintf("%s.%s", subscriptionID, resourceGroup) - -// For subscription-level resources -scope := subscriptionID -``` - -### 3. Attributes and Tags - -Convert Azure SDK structs to attributes and tags: - -```go -attributes, err := shared.ToAttributesWithExclude(vm, "tags") -tags := convertAzureTags(vm.Tags) // Convert map[string]*string to map[string]string -``` - -### 4. Health Status Mapping - -Map Azure resource provisioning states to SDP health statuses: - -```go -switch *vm.Properties.ProvisioningState { -case "Succeeded": - sdpItem.Health = sdp.Health_HEALTH_OK.Enum() -case "Creating", "Updating", "Deleting": - sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() -case "Failed", "Canceled": - sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() -default: - sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() -} -``` - -### 5. Multiple Query Parameters - -For resources requiring multiple parameters: - -```go -func (b resourceWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - resourceGroup := queryParts[0] // First parameter - resourceName := queryParts[1] // Second parameter - // ... implementation -} -``` - -### 6. Conditional Linked Queries - -Only create linked queries when the referenced resource exists: - -```go -if vm.Properties.StorageProfile != nil && vm.Properties.StorageProfile.OSDisk != nil { - if vm.Properties.StorageProfile.OSDisk.ManagedDisk != nil && vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID != nil { - // Create disk linked query - } -} -``` - -### 7. Resource ID Validation - -Always validate extracted resource IDs: - -```go -if resourceID != nil && *resourceID != "" { - resourceName := azureshared.ExtractResourceName(*resourceID) - if resourceName != "" { - // Use the extracted resource name - } -} -``` - -### 8. Adapter Registration - -Resource-group-scoped adapters are registered once per type with a slice of all resource group scopes. Build `resourceGroupScopes` after discovering resource groups, then pass it to each constructor: - -```go -// Build resource group scopes from discovered resource groups -resourceGroupScopes := make([]azureshared.ResourceGroupScope, 0, len(resourceGroups)) -for _, rg := range resourceGroups { - resourceGroupScopes = append(resourceGroupScopes, azureshared.NewResourceGroupScope(subscriptionID, rg)) -} - -// Multi-scope resource group adapters (one adapter per type handling all resource groups) -if len(resourceGroupScopes) > 0 { - adapters = append(adapters, - sources.WrapperToAdapter(NewComputeVirtualMachine( - clients.NewVirtualMachinesClient(vmClient), - resourceGroupScopes, - ), cache), - sources.WrapperToAdapter(NewStorageAccount(..., resourceGroupScopes), cache), - // ... one line per resource type (33 total) - ) -} - -// Subscription-level adapters are registered separately with subscriptionID -adapters = append(adapters, - sources.WrapperToAdapter(NewSubscription( - clients.NewSubscriptionClient(subClient), - subscriptionID, - ), cache), -) -``` - -### 9. Documentation Comments - -Always include Azure REST API documentation URLs in comments: - -```go -// Get retrieves a virtual machine by its name -// Reference: https://learn.microsoft.com/en-us/rest/api/compute/virtual-machines/get -// GET /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName} -``` - -### 10. Error Message Consistency - -Use consistent error messages and include context: - -```go -return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("invalid virtual machine name: %s", vmName), -} -``` - -## Validation Checklist - -Before submitting a new adapter, ensure: - -- [ ] File follows naming convention (`{resource-name}.go`) -- [ ] Imports are properly organized and minimal -- [ ] Wrapper type matches Azure API capabilities -- [ ] Base struct matches resource scope (MultiResourceGroupBase/Subscription) -- [ ] All required methods implemented (IAMPermissions, PredefinedRole, PotentialLinks) -- [ ] Terraform mappings are correct and include documentation URLs -- [ ] Get/Search lookups match the resource's query parameters -- [ ] Linked item queries use proper blast propagation -- [ ] Error handling uses `azureshared.QueryError` -- [ ] Pager pattern is correctly implemented for list operations -- [ ] Health status mapping is implemented (if applicable) -- [ ] Test file covers all required test cases -- [ ] Static tests validate linked item queries -- [ ] Client interface is properly defined in clients package -- [ ] Adapter is registered in the main adapters file -- [ ] Documentation comments include Azure REST API URLs -- [ ] Unique attribute consistency is maintained -- [ ] Scope handling uses appropriate helper functions -- [ ] Resource ID extraction is validated -- [ ] Conditional linked queries are properly implemented - -## Examples Reference - -For complete examples, refer to these existing adapters: - -- **Simple ListableWrapper**: `sources/azure/manual/compute-virtual-machine.go` -- **Resource Group Scoped Resource**: `sources/azure/manual/compute-virtual-machine.go` -- **Subscription-level Resource**: (to be added) -- **Complex Linked Queries**: (to be added) - -These examples demonstrate all the patterns and best practices outlined in this document. diff --git a/sources/azure/manual/README.md b/sources/azure/manual/README.md index fff9087b..ee64ddcc 100644 --- a/sources/azure/manual/README.md +++ b/sources/azure/manual/README.md @@ -110,10 +110,6 @@ t.Run("StaticTests", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // ... more test cases } diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 1b1a0697..ba2172fa 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -19,8 +19,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -238,6 +238,45 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred if err != nil { return nil, fmt.Errorf("failed to create zones client: %w", err) } + diskAccessesClient, err := armcompute.NewDiskAccessesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create disk accesses client: %w", err) + } + + dedicatedHostGroupsClient, err := armcompute.NewDedicatedHostGroupsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create dedicated host groups client: %w", err) + } + + capacityReservationGroupsClient, err := armcompute.NewCapacityReservationGroupsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create capacity reservation groups client: %w", err) + } + + galleryApplicationVersionsClient, err := armcompute.NewGalleryApplicationVersionsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create gallery application versions client: %w", err) + } + + galleryImagesClient, err := armcompute.NewGalleryImagesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create gallery images client: %w", err) + } + + galleriesClient, err := armcompute.NewGalleriesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create galleries client: %w", err) + } + + snapshotsClient, err := armcompute.NewSnapshotsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create snapshots client: %w", err) + } + + sharedGalleryImagesClient, err := armcompute.NewSharedGalleryImagesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create shared gallery images client: %w", err) + } // Multi-scope resource group adapters (one adapter per type handling all resource groups) if len(resourceGroupScopes) > 0 { @@ -374,9 +413,45 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewProximityPlacementGroupsClient(proximityPlacementGroupsClient), resourceGroupScopes, ), cache), - ) + sources.WrapperToAdapter(NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessesClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeGallery( + clients.NewGalleriesClient(galleriesClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeGalleryImage( + clients.NewGalleryImagesClient(galleryImagesClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotsClient), + resourceGroupScopes, + ), cache), + ) } + // Subscription-scoped adapters (not resource-group-scoped) + adapters = append(adapters, + sources.WrapperToAdapter(NewComputeSharedGalleryImage( + clients.NewSharedGalleryImagesClient(sharedGalleryImagesClient), + subscriptionID, + ), cache), + ) + log.WithFields(log.Fields{ "ovm.source.subscription_id": subscriptionID, "ovm.source.adapter_count": len(adapters), @@ -422,6 +497,14 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeVirtualMachineRunCommand(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeVirtualMachineExtension(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeProximityPlacementGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeDiskAccess(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeDedicatedHostGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeCapacityReservationGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeGalleryApplicationVersion(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeGallery(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeGalleryImage(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeSnapshot(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeSharedGalleryImage(nil, subscriptionID), noOpCache), ) _ = regions diff --git a/sources/azure/manual/authorization-role-assignment.go b/sources/azure/manual/authorization-role-assignment.go index 66f6f6eb..48d0f07a 100644 --- a/sources/azure/manual/authorization-role-assignment.go +++ b/sources/azure/manual/authorization-role-assignment.go @@ -6,15 +6,14 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) - var AuthorizationRoleAssignmentLookupByName = shared.NewItemTypeLookup("name", azureshared.AuthorizationRoleAssignment) type authorizationRoleAssignmentWrapper struct { @@ -62,7 +61,6 @@ func (a authorizationRoleAssignmentWrapper) List(ctx context.Context, scope stri return items, nil } - func (a authorizationRoleAssignmentWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { rgScope, err := a.ResourceGroupScopeFromScope(scope) if err != nil { @@ -169,12 +167,6 @@ func (a authorizationRoleAssignmentWrapper) azureRoleAssignmentToSDPItem(roleAss Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Role assignment depends on managed identity for delegated access - // If identity is deleted/modified, role assignment operations may fail - In: true, - Out: false, - }, }) } } @@ -207,12 +199,6 @@ func (a authorizationRoleAssignmentWrapper) azureRoleAssignmentToSDPItem(roleAss Query: roleDefinitionGUID, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Role assignment depends on role definition for permissions - // If role definition is deleted/modified, role assignment becomes invalid - In: true, - Out: false, - }, }) } } @@ -227,11 +213,41 @@ func (a authorizationRoleAssignmentWrapper) GetLookups() sources.ItemTypeLookups } } +// SearchLookups defines how the source can be searched (e.g. by role assignment name within a scope). +// Used when TerraformMethod is SEARCH (azurerm_role_assignment.id). +func (a authorizationRoleAssignmentWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + AuthorizationRoleAssignmentLookupByName, + }, + } +} + +// Search resolves a role assignment by name within the given scope. +// Supports Terraform SEARCH resolution when the query is the role assignment name (or extracted from Azure resource ID by the transformer). +func (a authorizationRoleAssignmentWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("Search requires 1 query part: roleAssignmentName"), scope, a.Type()) + } + roleAssignmentName := queryParts[0] + if roleAssignmentName == "" { + return nil, azureshared.QueryError(errors.New("roleAssignmentName cannot be empty"), scope, a.Type()) + } + item, qErr := a.Get(ctx, scope, roleAssignmentName) + if qErr != nil { + return nil, qErr + } + return []*sdp.Item{item}, nil +} + // ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment func (a authorizationRoleAssignmentWrapper) TerraformMappings() []*sdp.TerraformMapping { return []*sdp.TerraformMapping{ { - TerraformMethod: sdp.QueryMethod_GET, + TerraformMethod: sdp.QueryMethod_SEARCH, + // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment + // Terraform uses: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName} + // Or: /subscriptions/{sub}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName} TerraformQueryMap: "azurerm_role_assignment.id", }, } diff --git a/sources/azure/manual/authorization-role-assignment_test.go b/sources/azure/manual/authorization-role-assignment_test.go index 1635cdbe..ae5c5f92 100644 --- a/sources/azure/manual/authorization-role-assignment_test.go +++ b/sources/azure/manual/authorization-role-assignment_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -74,12 +74,7 @@ func TestAuthorizationRoleAssignment(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "b24988ac-6180-42a0-ab88-20f7382dd24c", ExpectedScope: subscriptionID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -103,17 +98,13 @@ func TestAuthorizationRoleAssignment(t *testing.T) { wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - // Test with insufficient query parts (empty) + // Test with empty query (adapter rejects before calling wrapper) _, qErr := adapter.Get(ctx, scope, "", true) if qErr == nil { t.Error("Expected error when getting role assignment with empty name, but got nil") } - - // Test with too many query parts - Get expects a single query string - _, qErr = adapter.Get(ctx, scope, shared.CompositeLookupKey("name", "extra"), true) - if qErr == nil { - t.Error("Expected error when getting role assignment with too many query parts, but got nil") - } + // Note: "too many" query parts are coalesced by the standard adapter into a single part, + // so the wrapper would receive one part and call the client. We only test empty here. }) t.Run("Get_EmptyRoleAssignmentName", func(t *testing.T) { @@ -335,6 +326,76 @@ func TestAuthorizationRoleAssignment(t *testing.T) { } }) + t.Run("Search", func(t *testing.T) { + roleAssignmentName := "test-role-assignment" + roleAssignment := createAzureRoleAssignment(roleAssignmentName, "/subscriptions/test-subscription/resourceGroups/test-rg") + + mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) + azureScope := "/subscriptions/test-subscription/resourceGroups/test-rg" + mockClient.EXPECT().Get(ctx, azureScope, roleAssignmentName, nil).Return( + armauthorization.RoleAssignmentsClientGetResponse{ + RoleAssignment: *roleAssignment, + }, nil) + + wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not implement SearchableAdapter") + } + + items, err := searchable.Search(ctx, scope, roleAssignmentName, true) + if err != nil { + t.Fatalf("Search failed: %v", err) + } + if len(items) != 1 { + t.Errorf("Expected 1 item, got %d", len(items)) + } + if len(items) > 0 && items[0].GetType() != azureshared.AuthorizationRoleAssignment.String() { + t.Errorf("Expected type %s, got %s", azureshared.AuthorizationRoleAssignment.String(), items[0].GetType()) + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) + wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + searchableWrapper := wrapper.(sources.SearchableWrapper) + + _, qErr := searchableWrapper.Search(ctx, scope, "name1", "name2") + if qErr == nil { + t.Error("Expected error for too many query parts, got nil") + } + }) + + t.Run("Search_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) + wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + searchableWrapper := wrapper.(sources.SearchableWrapper) + + _, qErr := searchableWrapper.Search(ctx, scope, "") + if qErr == nil { + t.Error("Expected error for empty role assignment name, got nil") + } + }) + + t.Run("SearchLookups", func(t *testing.T) { + mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) + wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + searchableWrapper := wrapper.(sources.SearchableWrapper) + + searchLookups := searchableWrapper.SearchLookups() + if len(searchLookups) != 1 { + t.Errorf("Expected 1 search lookup group, got %d", len(searchLookups)) + } + if len(searchLookups) > 0 && len(searchLookups[0]) != 1 { + t.Errorf("Expected 1 lookup in first group, got %d", len(searchLookups[0])) + } + if len(searchLookups) > 0 && len(searchLookups[0]) > 0 && searchLookups[0][0].ItemType != azureshared.AuthorizationRoleAssignment { + t.Errorf("Expected SearchLookups to include AuthorizationRoleAssignment, got %v", searchLookups[0][0].ItemType) + } + }) + t.Run("TerraformMappings", func(t *testing.T) { mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -348,8 +409,8 @@ func TestAuthorizationRoleAssignment(t *testing.T) { for _, mapping := range mappings { if mapping.GetTerraformQueryMap() == "azurerm_role_assignment.id" { foundMapping = true - if mapping.GetTerraformMethod() != sdp.QueryMethod_GET { - t.Errorf("Expected TerraformMethod to be GET, got: %v", mapping.GetTerraformMethod()) + if mapping.GetTerraformMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Expected TerraformMethod to be SEARCH, got: %v", mapping.GetTerraformMethod()) } break } @@ -437,23 +498,13 @@ func TestAuthorizationRoleAssignment(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "b24988ac-6180-42a0-ab88-20f7382dd24c", ExpectedScope: subscriptionID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Delegated Managed Identity link ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/batch-batch-accounts.go b/sources/azure/manual/batch-batch-accounts.go index cb78de1b..5dda3207 100644 --- a/sources/azure/manual/batch-batch-accounts.go +++ b/sources/azure/manual/batch-batch-accounts.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" ) var BatchAccountLookupByName = shared.NewItemTypeLookup("name", azureshared.BatchBatchAccount) @@ -128,12 +128,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: storageAccountName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on storage account for auto-storage functionality - // If storage account is deleted/modified, batch account operations may fail - In: true, - Out: false, - }, }) } } @@ -156,12 +150,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: keyVaultName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on Key Vault for certificate management and encryption - // If Key Vault is deleted/modified, batch account operations may fail - In: true, - Out: false, - }, }) } } @@ -188,12 +176,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: vaultName, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on Key Vault for customer-managed encryption keys - // If Key Vault is deleted/modified or key is rotated, batch account encryption may be affected - In: true, - Out: false, - }, }) } } @@ -219,12 +201,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: privateEndpointName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint connection is tightly coupled with the batch account - // Changes to either affect the other - In: true, - Out: true, - }, }) } } @@ -249,12 +225,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on managed identity for authentication - // If identity is deleted/modified, batch account operations may fail - In: true, - Out: false, - }, }) } } @@ -278,12 +248,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: nodeIdentityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account compute nodes depend on managed identity for auto-storage access - // If identity is deleted/modified, compute nodes may fail to access storage - In: true, - Out: false, - }, }) } } @@ -298,12 +262,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Applications are child resources of the batch account - // Changes to batch account affect applications, and vice versa - In: true, - Out: true, - }, }) // Link to Pools (child resource) @@ -316,12 +274,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Pools are child resources of the batch account - // Changes to batch account affect pools, and vice versa - In: true, - Out: true, - }, }) // Link to Certificates (child resource) @@ -334,12 +286,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Certificates are child resources of the batch account - // Changes to batch account affect certificates, and vice versa - In: true, - Out: true, - }, }) // Link to Private Endpoint Connections (child resource) @@ -352,12 +298,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint connections are child resources of the batch account - // Changes to batch account affect connections, and vice versa - In: true, - Out: true, - }, }) // Link to Private Link Resources (child resource) @@ -370,12 +310,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private link resources are child resources of the batch account - // Changes to batch account affect resources, and vice versa - In: true, - Out: true, - }, }) // Link to Detectors (child resource) @@ -388,12 +322,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Detectors are child resources of the batch account - // Changes to batch account affect detectors, and vice versa - In: true, - Out: true, - }, }) // Link to DNS name (standard library) if AccountEndpoint is configured @@ -405,11 +333,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: *account.Properties.AccountEndpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are shared resources; changes can affect connectivity both ways - In: true, - Out: true, - }, }) } @@ -425,11 +348,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: *ipRule.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are shared resources; changes can affect connectivity both ways - In: true, - Out: true, - }, }) } } @@ -448,11 +366,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: *ipRule.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are shared resources; changes can affect connectivity both ways - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/batch-batch-accounts_test.go b/sources/azure/manual/batch-batch-accounts_test.go index bd9d37a8..8ecab3af 100644 --- a/sources/azure/manual/batch-batch-accounts_test.go +++ b/sources/azure/manual/batch-batch-accounts_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -87,122 +87,67 @@ func TestBatchAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-storage-account", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Key Vault link ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Private Endpoint link ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // User Assigned Managed Identity link ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Node Identity Reference link ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-node-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Applications (child resource) ExpectedType: azureshared.BatchBatchApplication.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Pools (child resource) ExpectedType: azureshared.BatchBatchPool.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Certificates (child resource) ExpectedType: azureshared.BatchBatchCertificate.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint Connections (child resource) ExpectedType: azureshared.BatchBatchPrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Link Resources (child resource) ExpectedType: azureshared.BatchBatchPrivateLinkResource.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Detectors (child resource) ExpectedType: azureshared.BatchBatchDetector.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-availability-set.go b/sources/azure/manual/compute-availability-set.go index 8fed33fb..97126244 100644 --- a/sources/azure/manual/compute-availability-set.go +++ b/sources/azure/manual/compute-availability-set.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -149,10 +149,6 @@ func (c computeAvailabilitySetWrapper) azureAvailabilitySetToSDPItem(availabilit Query: ppgName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If PPG changes → Availability Set placement changes (In: true) - Out: false, // If Availability Set is deleted → PPG remains (Out: false) - }, }) } } @@ -176,10 +172,6 @@ func (c computeAvailabilitySetWrapper) azureAvailabilitySetToSDPItem(availabilit Query: vmName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM changes → Availability Set membership changes (In: true) - Out: false, // If Availability Set is deleted → VMs remain but lose availability set association (Out: false) - }, }) } } diff --git a/sources/azure/manual/compute-availability-set_test.go b/sources/azure/manual/compute-availability-set_test.go index ad5cd317..12b22270 100644 --- a/sources/azure/manual/compute-availability-set_test.go +++ b/sources/azure/manual/compute-availability-set_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -71,34 +71,19 @@ func TestComputeAvailabilitySet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-ppg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.VirtualMachines[0].ID ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm-1", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.VirtualMachines[1].ID ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm-2", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-capacity-reservation-group.go b/sources/azure/manual/compute-capacity-reservation-group.go new file mode 100644 index 00000000..25caaa53 --- /dev/null +++ b/sources/azure/manual/compute-capacity-reservation-group.go @@ -0,0 +1,229 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeCapacityReservationGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeCapacityReservationGroup) + +type computeCapacityReservationGroupWrapper struct { + client clients.CapacityReservationGroupsClient + *azureshared.MultiResourceGroupBase +} + +// NewComputeCapacityReservationGroup creates a new computeCapacityReservationGroupWrapper instance. +func NewComputeCapacityReservationGroup(client clients.CapacityReservationGroupsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeCapacityReservationGroupWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeCapacityReservationGroup, + ), + } +} + +func capacityReservationGroupGetOptions() *armcompute.CapacityReservationGroupsClientGetOptions { + expand := armcompute.CapacityReservationGroupInstanceViewTypes(armcompute.ExpandTypesForGetCapacityReservationGroupsVirtualMachinesRef) + return &armcompute.CapacityReservationGroupsClientGetOptions{ + Expand: &expand, + } +} + +func capacityReservationGroupListOptions() *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions { + expand := armcompute.ExpandTypesForGetCapacityReservationGroupsVirtualMachinesRef + return &armcompute.CapacityReservationGroupsClientListByResourceGroupOptions{ + Expand: &expand, + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/capacity-reservation-groups/get?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeCapacityReservationGroupWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the capacity reservation group name"), scope, c.Type()) + } + capacityReservationGroupName := queryParts[0] + if capacityReservationGroupName == "" { + return nil, azureshared.QueryError(errors.New("capacity reservation group name cannot be empty"), scope, c.Type()) + } + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + capacityReservationGroup, err := c.client.Get(ctx, rgScope.ResourceGroup, capacityReservationGroupName, capacityReservationGroupGetOptions()) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureCapacityReservationGroupToSDPItem(&capacityReservationGroup.CapacityReservationGroup, scope) +} + +// ref:https://learn.microsoft.com/en-us/rest/api/compute/capacity-reservation-groups/list-by-resource-group?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeCapacityReservationGroupWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, capacityReservationGroupListOptions()) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, capacityReservationGroup := range page.Value { + if capacityReservationGroup.Name == nil { + continue + } + item, sdpErr := c.azureCapacityReservationGroupToSDPItem(capacityReservationGroup, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c *computeCapacityReservationGroupWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, capacityReservationGroupListOptions()) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, capacityReservationGroup := range page.Value { + if capacityReservationGroup.Name == nil { + continue + } + item, sdpErr := c.azureCapacityReservationGroupToSDPItem(capacityReservationGroup, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c *computeCapacityReservationGroupWrapper) azureCapacityReservationGroupToSDPItem(capacityReservationGroup *armcompute.CapacityReservationGroup, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(capacityReservationGroup, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + if capacityReservationGroup.Properties != nil { + groupName := "" + if capacityReservationGroup.Name != nil { + groupName = *capacityReservationGroup.Name + } + + // Child resource: capacity reservations in this group (have their own GET/LIST endpoints) + if capacityReservationGroup.Properties.CapacityReservations != nil && groupName != "" { + for _, ref := range capacityReservationGroup.Properties.CapacityReservations { + if ref == nil || ref.ID == nil || *ref.ID == "" { + continue + } + reservationName := azureshared.ExtractResourceName(*ref.ID) + if reservationName == "" { + continue + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeCapacityReservation.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(groupName, reservationName), + Scope: scope, + }, + }) + } + } + + // External resource: VMs associated with this capacity reservation group + if capacityReservationGroup.Properties.VirtualMachinesAssociated != nil { + for _, ref := range capacityReservationGroup.Properties.VirtualMachinesAssociated { + if ref == nil || ref.ID == nil || *ref.ID == "" { + continue + } + vmName := azureshared.ExtractResourceName(*ref.ID) + if vmName == "" { + continue + } + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(*ref.ID); extractedScope != "" { + linkScope = extractedScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeVirtualMachine.String(), + Method: sdp.QueryMethod_GET, + Query: vmName, + Scope: linkScope, + }, + }) + } + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeCapacityReservationGroup.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(capacityReservationGroup.Tags), + LinkedItemQueries: linkedItemQueries, + } + + return sdpItem, nil +} +func (c *computeCapacityReservationGroupWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeCapacityReservationGroupLookupByName, + } +} + +func (c *computeCapacityReservationGroupWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeCapacityReservation: true, + azureshared.ComputeVirtualMachine: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/capacity_reservation_group +func (c *computeCapacityReservationGroupWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_capacity_reservation_group.name", + }, + } +} + +func (c *computeCapacityReservationGroupWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/capacityReservationGroups/read", + } +} + +func (c *computeCapacityReservationGroupWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-capacity-reservation-group_test.go b/sources/azure/manual/compute-capacity-reservation-group_test.go new file mode 100644 index 00000000..fa22c505 --- /dev/null +++ b/sources/azure/manual/compute-capacity-reservation-group_test.go @@ -0,0 +1,429 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestComputeCapacityReservationGroup(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + + t.Run("Get", func(t *testing.T) { + groupName := "test-crg" + crg := createAzureCapacityReservationGroup(groupName) + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, gomock.Eq(capacityReservationGroupGetOptions())).Return( + armcompute.CapacityReservationGroupsClientGetResponse{ + CapacityReservationGroup: *crg, + }, nil) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, groupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeCapacityReservationGroup.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeCapacityReservationGroup.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != groupName { + t.Errorf("Expected unique attribute value %s, got %s", groupName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithLinkedResources", func(t *testing.T) { + groupName := "test-crg-with-links" + crg := createAzureCapacityReservationGroupWithLinks(groupName, subscriptionID, resourceGroup, []string{"res-1", "res-2"}, []string{"vm-1", "vm-2"}) + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, gomock.Eq(capacityReservationGroupGetOptions())).Return( + armcompute.CapacityReservationGroupsClientGetResponse{ + CapacityReservationGroup: *crg, + }, nil) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, groupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ComputeCapacityReservation.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(groupName, "res-1"), + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.ComputeCapacityReservation.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(groupName, "res-2"), + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.ComputeVirtualMachine.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "vm-1", + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.ComputeVirtualMachine.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "vm-2", + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, scope) + if qErr == nil { + t.Error("Expected error when getting with no query parts, but got nil") + } + }) + + t.Run("Get_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting with empty name, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("capacity reservation group not found") + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent", gomock.Eq(capacityReservationGroupGetOptions())).Return( + armcompute.CapacityReservationGroupsClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent", true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + crg1 := createAzureCapacityReservationGroup("test-crg-1") + crg2 := createAzureCapacityReservationGroup("test-crg-2") + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crg2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(mockPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + if item.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %s", item.GetTags()["env"]) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + crg1 := createAzureCapacityReservationGroup("test-crg-1") + crg2 := createAzureCapacityReservationGroup("test-crg-2") + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crg2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(mockPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + crg1 := createAzureCapacityReservationGroup("test-crg-1") + crgNilName := &armcompute.CapacityReservationGroup{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.CapacityReservationGroupProperties{}, + } + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crgNilName}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(mockPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ListWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + errorPager := newErrorCapacityReservationGroupsPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(errorPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + _, err := listable.List(ctx, scope, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("ListStreamWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + errorPager := newErrorCapacityReservationGroupsPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(errorPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(func(item *sdp.Item) {}, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + + if len(errs) == 0 { + t.Error("Expected error when pager returns error, but got none") + } + }) +} + +func capacityReservationGroupGetOptions() *armcompute.CapacityReservationGroupsClientGetOptions { + expand := armcompute.CapacityReservationGroupInstanceViewTypes(armcompute.ExpandTypesForGetCapacityReservationGroupsVirtualMachinesRef) + return &armcompute.CapacityReservationGroupsClientGetOptions{ + Expand: &expand, + } +} + +func capacityReservationGroupListOptions() *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions { + expand := armcompute.ExpandTypesForGetCapacityReservationGroupsVirtualMachinesRef + return &armcompute.CapacityReservationGroupsClientListByResourceGroupOptions{ + Expand: &expand, + } +} + +// createAzureCapacityReservationGroup creates a mock Azure Capacity Reservation Group for testing. +func createAzureCapacityReservationGroup(groupName string) *armcompute.CapacityReservationGroup { + return &armcompute.CapacityReservationGroup{ + Name: to.Ptr(groupName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.CapacityReservationGroupProperties{}, + } +} + +// createAzureCapacityReservationGroupWithLinks creates a mock group with capacity reservation and VM links. +func createAzureCapacityReservationGroupWithLinks(groupName, subscriptionID, resourceGroup string, reservationNames, vmNames []string) *armcompute.CapacityReservationGroup { + reservations := make([]*armcompute.SubResourceReadOnly, 0, len(reservationNames)) + for _, name := range reservationNames { + reservations = append(reservations, &armcompute.SubResourceReadOnly{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/capacityReservationGroups/" + groupName + "/capacityReservations/" + name), + }) + } + vms := make([]*armcompute.SubResourceReadOnly, 0, len(vmNames)) + for _, name := range vmNames { + vms = append(vms, &armcompute.SubResourceReadOnly{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/" + name), + }) + } + return &armcompute.CapacityReservationGroup{ + Name: to.Ptr(groupName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.CapacityReservationGroupProperties{ + CapacityReservations: reservations, + VirtualMachinesAssociated: vms, + }, + } +} + +// mockCapacityReservationGroupsPager is a mock pager for CapacityReservationGroupsClientListByResourceGroupResponse. +type mockCapacityReservationGroupsPager struct { + ctrl *gomock.Controller + items []*armcompute.CapacityReservationGroup + index int + more bool +} + +func newMockCapacityReservationGroupsPager(ctrl *gomock.Controller, items []*armcompute.CapacityReservationGroup) clients.CapacityReservationGroupsPager { + return &mockCapacityReservationGroupsPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockCapacityReservationGroupsPager) More() bool { + return m.more +} + +func (m *mockCapacityReservationGroupsPager) NextPage(ctx context.Context) (armcompute.CapacityReservationGroupsClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.CapacityReservationGroupsClientListByResourceGroupResponse{ + CapacityReservationGroupListResult: armcompute.CapacityReservationGroupListResult{ + Value: []*armcompute.CapacityReservationGroup{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.CapacityReservationGroupsClientListByResourceGroupResponse{ + CapacityReservationGroupListResult: armcompute.CapacityReservationGroupListResult{ + Value: []*armcompute.CapacityReservationGroup{item}, + }, + }, nil +} + +// errorCapacityReservationGroupsPager is a mock pager that always returns an error. +type errorCapacityReservationGroupsPager struct { + ctrl *gomock.Controller +} + +func newErrorCapacityReservationGroupsPager(ctrl *gomock.Controller) clients.CapacityReservationGroupsPager { + return &errorCapacityReservationGroupsPager{ctrl: ctrl} +} + +func (e *errorCapacityReservationGroupsPager) More() bool { + return true +} + +func (e *errorCapacityReservationGroupsPager) NextPage(ctx context.Context) (armcompute.CapacityReservationGroupsClientListByResourceGroupResponse, error) { + return armcompute.CapacityReservationGroupsClientListByResourceGroupResponse{}, errors.New("pager error") +} diff --git a/sources/azure/manual/compute-dedicated-host-group.go b/sources/azure/manual/compute-dedicated-host-group.go new file mode 100644 index 00000000..2dc9d4ed --- /dev/null +++ b/sources/azure/manual/compute-dedicated-host-group.go @@ -0,0 +1,180 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeDedicatedHostGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeDedicatedHostGroup) + +type computeDedicatedHostGroupWrapper struct { + client clients.DedicatedHostGroupsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeDedicatedHostGroup(client clients.DedicatedHostGroupsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeDedicatedHostGroupWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeDedicatedHostGroup, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/dedicated-host-groups/get?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeDedicatedHostGroupWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the dedicated host group name"), scope, c.Type()) + } + dedicatedHostGroupName := queryParts[0] + if dedicatedHostGroupName == "" { + return nil, azureshared.QueryError(errors.New("dedicated host group name cannot be empty"), scope, c.Type()) + } + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + dedicatedHostGroup, err := c.client.Get(ctx, rgScope.ResourceGroup, dedicatedHostGroupName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureDedicatedHostGroupToSDPItem(&dedicatedHostGroup.DedicatedHostGroup, scope) +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/dedicated-host-groups/list-by-resource-group?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeDedicatedHostGroupWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, dedicatedHostGroup := range page.Value { + if dedicatedHostGroup.Name == nil { + continue + } + item, sdpErr := c.azureDedicatedHostGroupToSDPItem(dedicatedHostGroup, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c *computeDedicatedHostGroupWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, dedicatedHostGroup := range page.Value { + if dedicatedHostGroup.Name == nil { + continue + } + item, sdpErr := c.azureDedicatedHostGroupToSDPItem(dedicatedHostGroup, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c *computeDedicatedHostGroupWrapper) azureDedicatedHostGroupToSDPItem(dedicatedHostGroup *armcompute.DedicatedHostGroup, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(dedicatedHostGroup, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + if dedicatedHostGroup.Properties != nil && dedicatedHostGroup.Properties.Hosts != nil && dedicatedHostGroup.Name != nil { + hostGroupName := *dedicatedHostGroup.Name + for _, hostRef := range dedicatedHostGroup.Properties.Hosts { + if hostRef == nil || hostRef.ID == nil || *hostRef.ID == "" { + continue + } + hostName := azureshared.ExtractResourceName(*hostRef.ID) + if hostName == "" { + continue + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDedicatedHost.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(hostGroupName, hostName), + Scope: scope, + }, + }) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeDedicatedHostGroup.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(dedicatedHostGroup.Tags), + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c *computeDedicatedHostGroupWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeDedicatedHostGroupLookupByName, + } +} + +func (c *computeDedicatedHostGroupWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeDedicatedHost: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/dedicated_host_group +func (c *computeDedicatedHostGroupWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_dedicated_host_group.name", + }, + } +} + +func (c *computeDedicatedHostGroupWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/hostGroups/read", + } +} + +func (c *computeDedicatedHostGroupWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-dedicated-host-group_test.go b/sources/azure/manual/compute-dedicated-host-group_test.go new file mode 100644 index 00000000..5b24242f --- /dev/null +++ b/sources/azure/manual/compute-dedicated-host-group_test.go @@ -0,0 +1,403 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestComputeDedicatedHostGroup(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + + t.Run("Get", func(t *testing.T) { + hostGroupName := "test-host-group" + dedicatedHostGroup := createAzureDedicatedHostGroup(hostGroupName) + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hostGroupName, nil).Return( + armcompute.DedicatedHostGroupsClientGetResponse{ + DedicatedHostGroup: *dedicatedHostGroup, + }, nil) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, hostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDedicatedHostGroup.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeDedicatedHostGroup.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != hostGroupName { + t.Errorf("Expected unique attribute value %s, got %s", hostGroupName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithHosts", func(t *testing.T) { + hostGroupName := "test-host-group-with-hosts" + dedicatedHostGroup := createAzureDedicatedHostGroupWithHosts(hostGroupName, subscriptionID, resourceGroup, "host-1", "host-2") + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hostGroupName, nil).Return( + armcompute.DedicatedHostGroupsClientGetResponse{ + DedicatedHostGroup: *dedicatedHostGroup, + }, nil) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, hostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ComputeDedicatedHost.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(hostGroupName, "host-1"), + ExpectedScope: scope, + }, { + ExpectedType: azureshared.ComputeDedicatedHost.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(hostGroupName, "host-2"), + ExpectedScope: scope, + }} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, scope) + if qErr == nil { + t.Error("Expected error when getting with no query parts, but got nil") + } + }) + + t.Run("Get_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting with empty name, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("dedicated host group not found") + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent", nil).Return( + armcompute.DedicatedHostGroupsClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent", true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + hostGroup1 := createAzureDedicatedHostGroup("test-host-group-1") + hostGroup2 := createAzureDedicatedHostGroup("test-host-group-2") + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockPager := newMockDedicatedHostGroupsPager(ctrl, []*armcompute.DedicatedHostGroup{hostGroup1, hostGroup2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + if item.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %s", item.GetTags()["env"]) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + hostGroup1 := createAzureDedicatedHostGroup("test-host-group-1") + hostGroup2 := createAzureDedicatedHostGroup("test-host-group-2") + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockPager := newMockDedicatedHostGroupsPager(ctrl, []*armcompute.DedicatedHostGroup{hostGroup1, hostGroup2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + hostGroup1 := createAzureDedicatedHostGroup("test-host-group-1") + hostGroupNilName := &armcompute.DedicatedHostGroup{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.DedicatedHostGroupProperties{ + PlatformFaultDomainCount: to.Ptr(int32(2)), + }, + } + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockPager := newMockDedicatedHostGroupsPager(ctrl, []*armcompute.DedicatedHostGroup{hostGroup1, hostGroupNilName}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ListWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + errorPager := newErrorDedicatedHostGroupsPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + _, err := listable.List(ctx, scope, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("ListStreamWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + errorPager := newErrorDedicatedHostGroupsPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(func(item *sdp.Item) {}, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + + if len(errs) == 0 { + t.Error("Expected error when pager returns error, but got none") + } + }) +} + +// createAzureDedicatedHostGroup creates a mock Azure Dedicated Host Group for testing. +func createAzureDedicatedHostGroup(hostGroupName string) *armcompute.DedicatedHostGroup { + return &armcompute.DedicatedHostGroup{ + Name: to.Ptr(hostGroupName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.DedicatedHostGroupProperties{ + PlatformFaultDomainCount: to.Ptr(int32(2)), + SupportAutomaticPlacement: to.Ptr(false), + AdditionalCapabilities: nil, + Hosts: nil, + InstanceView: nil, + }, + } +} + +// createAzureDedicatedHostGroupWithHosts creates a mock Azure Dedicated Host Group with host references. +func createAzureDedicatedHostGroupWithHosts(hostGroupName, subscriptionID, resourceGroup string, hostNames ...string) *armcompute.DedicatedHostGroup { + hosts := make([]*armcompute.SubResourceReadOnly, 0, len(hostNames)) + for _, name := range hostNames { + hosts = append(hosts, &armcompute.SubResourceReadOnly{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/hostGroups/" + hostGroupName + "/hosts/" + name), + }) + } + return &armcompute.DedicatedHostGroup{ + Name: to.Ptr(hostGroupName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.DedicatedHostGroupProperties{ + PlatformFaultDomainCount: to.Ptr(int32(2)), + Hosts: hosts, + }, + } +} + +// mockDedicatedHostGroupsPager is a mock pager for DedicatedHostGroupsClientListByResourceGroupResponse. +type mockDedicatedHostGroupsPager struct { + ctrl *gomock.Controller + items []*armcompute.DedicatedHostGroup + index int + more bool +} + +func newMockDedicatedHostGroupsPager(ctrl *gomock.Controller, items []*armcompute.DedicatedHostGroup) clients.DedicatedHostGroupsPager { + return &mockDedicatedHostGroupsPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockDedicatedHostGroupsPager) More() bool { + return m.more +} + +func (m *mockDedicatedHostGroupsPager) NextPage(ctx context.Context) (armcompute.DedicatedHostGroupsClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.DedicatedHostGroupsClientListByResourceGroupResponse{ + DedicatedHostGroupListResult: armcompute.DedicatedHostGroupListResult{ + Value: []*armcompute.DedicatedHostGroup{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.DedicatedHostGroupsClientListByResourceGroupResponse{ + DedicatedHostGroupListResult: armcompute.DedicatedHostGroupListResult{ + Value: []*armcompute.DedicatedHostGroup{item}, + }, + }, nil +} + +// errorDedicatedHostGroupsPager is a mock pager that always returns an error. +type errorDedicatedHostGroupsPager struct { + ctrl *gomock.Controller +} + +func newErrorDedicatedHostGroupsPager(ctrl *gomock.Controller) clients.DedicatedHostGroupsPager { + return &errorDedicatedHostGroupsPager{ctrl: ctrl} +} + +func (e *errorDedicatedHostGroupsPager) More() bool { + return true +} + +func (e *errorDedicatedHostGroupsPager) NextPage(ctx context.Context) (armcompute.DedicatedHostGroupsClientListByResourceGroupResponse, error) { + return armcompute.DedicatedHostGroupsClientListByResourceGroupResponse{}, errors.New("pager error") +} diff --git a/sources/azure/manual/compute-disk-access.go b/sources/azure/manual/compute-disk-access.go new file mode 100644 index 00000000..15b821cd --- /dev/null +++ b/sources/azure/manual/compute-disk-access.go @@ -0,0 +1,185 @@ +package manual + +import ( + "context" + "errors" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + discovery "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + sdpcache "github.com/overmindtech/cli/go/sdpcache" + sources "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeDiskAccessLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeDiskAccess) + +type computeDiskAccessWrapper struct { + client clients.DiskAccessesClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeDiskAccess(client clients.DiskAccessesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeDiskAccessWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_STORAGE, + azureshared.ComputeDiskAccess, + ), + } +} + +func (c *computeDiskAccessWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the disk access name"), scope, c.Type()) + } + diskAccessName := queryParts[0] + if diskAccessName == "" { + return nil, azureshared.QueryError(errors.New("disk access name cannot be empty"), scope, c.Type()) + } + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + diskAccess, err := c.client.Get(ctx, rgScope.ResourceGroup, diskAccessName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureDiskAccessToSDPItem(&diskAccess.DiskAccess, scope) +} + +func (c *computeDiskAccessWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, diskAccess := range page.Value { + if diskAccess.Name == nil { + continue + } + item, sdpErr := c.azureDiskAccessToSDPItem(diskAccess, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c *computeDiskAccessWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, diskAccess := range page.Value { + if diskAccess.Name == nil { + continue + } + item, sdpErr := c.azureDiskAccessToSDPItem(diskAccess, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} +func (c *computeDiskAccessWrapper) azureDiskAccessToSDPItem(diskAccess *armcompute.DiskAccess, scope string) (*sdp.Item, *sdp.QueryError) { + if diskAccess.Name == nil { + return nil, azureshared.QueryError(errors.New("name is nil"), scope, c.Type()) + } + attributes, err := shared.ToAttributesWithExclude(diskAccess, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + sdpItem := &sdp.Item{ + Type: azureshared.ComputeDiskAccess.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(diskAccess.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Link to Private Endpoint Connections (child resource) + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disk-accesses/list-private-endpoint-connections + // GET /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/diskAccesses/{diskAccessName}/privateEndpointConnections + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskAccessPrivateEndpointConnection.String(), + Method: sdp.QueryMethod_SEARCH, + Query: *diskAccess.Name, + Scope: scope, + }, + }) + + // Link to Network Private Endpoints (external resources) from PrivateEndpointConnections + // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/private-endpoints/get + if diskAccess.Properties != nil && diskAccess.Properties.PrivateEndpointConnections != nil { + for _, peConnection := range diskAccess.Properties.PrivateEndpointConnections { + if peConnection.Properties != nil && peConnection.Properties.PrivateEndpoint != nil && peConnection.Properties.PrivateEndpoint.ID != nil { + privateEndpointID := *peConnection.Properties.PrivateEndpoint.ID + privateEndpointName := azureshared.ExtractResourceName(privateEndpointID) + if privateEndpointName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(privateEndpointID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + Query: privateEndpointName, + Scope: extractedScope, + }, + }) + } + } + } + } + + return sdpItem, nil +} + +func (c *computeDiskAccessWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeDiskAccessLookupByName, + } +} + +func (c *computeDiskAccessWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeDiskAccessPrivateEndpointConnection: true, + azureshared.NetworkPrivateEndpoint: true, + } +} + +func (c *computeDiskAccessWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_disk_access.name", + }, + } +} diff --git a/sources/azure/manual/compute-disk-access_test.go b/sources/azure/manual/compute-disk-access_test.go new file mode 100644 index 00000000..9b042210 --- /dev/null +++ b/sources/azure/manual/compute-disk-access_test.go @@ -0,0 +1,421 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestComputeDiskAccess(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + + t.Run("Get", func(t *testing.T) { + diskAccessName := "test-disk-access" + diskAccess := createAzureDiskAccess(diskAccessName) + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, diskAccessName, nil).Return( + armcompute.DiskAccessesClientGetResponse{ + DiskAccess: *diskAccess, + }, nil) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, diskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDiskAccess.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeDiskAccess.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != diskAccessName { + t.Errorf("Expected unique attribute value %s, got %s", diskAccessName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Child resource: Private Endpoint Connections + ExpectedType: azureshared.ComputeDiskAccessPrivateEndpointConnection.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: diskAccessName, + ExpectedScope: scope, + }} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithPrivateEndpointConnections", func(t *testing.T) { + diskAccessName := "test-disk-access-with-pe" + diskAccess := createAzureDiskAccessWithPrivateEndpointConnections(diskAccessName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, diskAccessName, nil).Return( + armcompute.DiskAccessesClientGetResponse{ + DiskAccess: *diskAccess, + }, nil) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, diskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ComputeDiskAccessPrivateEndpointConnection.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: diskAccessName, + ExpectedScope: scope, + }, { + // Network Private Endpoint (same resource group) + ExpectedType: azureshared.NetworkPrivateEndpoint.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-private-endpoint", + ExpectedScope: scope, + }, { + // Network Private Endpoint (different resource group) + ExpectedType: azureshared.NetworkPrivateEndpoint.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-private-endpoint-other-rg", + ExpectedScope: subscriptionID + ".other-rg", + }} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, scope) + if qErr == nil { + t.Error("Expected error when getting with no query parts, but got nil") + } + }) + + t.Run("Get_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting with empty name, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("disk access not found") + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent", nil).Return( + armcompute.DiskAccessesClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent", true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + diskAccess1 := createAzureDiskAccess("test-disk-access-1") + diskAccess2 := createAzureDiskAccess("test-disk-access-2") + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockPager := newMockDiskAccessesPager(ctrl, []*armcompute.DiskAccess{diskAccess1, diskAccess2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + if item.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %s", item.GetTags()["env"]) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + diskAccess1 := createAzureDiskAccess("test-disk-access-1") + diskAccess2 := createAzureDiskAccess("test-disk-access-2") + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockPager := newMockDiskAccessesPager(ctrl, []*armcompute.DiskAccess{diskAccess1, diskAccess2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + diskAccess1 := createAzureDiskAccess("test-disk-access-1") + diskAccessNilName := &armcompute.DiskAccess{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + } + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockPager := newMockDiskAccessesPager(ctrl, []*armcompute.DiskAccess{diskAccess1, diskAccessNilName}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ListWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + errorPager := newErrorDiskAccessesPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + _, err := listable.List(ctx, scope, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("ListStreamWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + errorPager := newErrorDiskAccessesPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(func(item *sdp.Item) {}, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + + if len(errs) == 0 { + t.Error("Expected error when pager returns error, but got none") + } + }) +} + +// createAzureDiskAccess creates a mock Azure Disk Access for testing. +func createAzureDiskAccess(diskAccessName string) *armcompute.DiskAccess { + return &armcompute.DiskAccess{ + Name: to.Ptr(diskAccessName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.DiskAccessProperties{ + ProvisioningState: to.Ptr("Succeeded"), + }, + } +} + +// createAzureDiskAccessWithPrivateEndpointConnections creates a mock Azure Disk Access with private endpoint connections. +func createAzureDiskAccessWithPrivateEndpointConnections(diskAccessName, subscriptionID, resourceGroup string) *armcompute.DiskAccess { + return &armcompute.DiskAccess{ + Name: to.Ptr(diskAccessName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.DiskAccessProperties{ + ProvisioningState: to.Ptr("Succeeded"), + PrivateEndpointConnections: []*armcompute.PrivateEndpointConnection{ + { + Name: to.Ptr("pe-connection-1"), + Properties: &armcompute.PrivateEndpointConnectionProperties{ + PrivateEndpoint: &armcompute.PrivateEndpoint{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), + }, + }, + }, + { + Name: to.Ptr("pe-connection-2"), + Properties: &armcompute.PrivateEndpointConnectionProperties{ + PrivateEndpoint: &armcompute.PrivateEndpoint{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-other-rg"), + }, + }, + }, + }, + }, + } +} + +// mockDiskAccessesPager is a mock pager for DiskAccessesClientListByResourceGroupResponse. +type mockDiskAccessesPager struct { + ctrl *gomock.Controller + items []*armcompute.DiskAccess + index int + more bool +} + +func newMockDiskAccessesPager(ctrl *gomock.Controller, items []*armcompute.DiskAccess) clients.DiskAccessesPager { + return &mockDiskAccessesPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockDiskAccessesPager) More() bool { + return m.more +} + +func (m *mockDiskAccessesPager) NextPage(ctx context.Context) (armcompute.DiskAccessesClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.DiskAccessesClientListByResourceGroupResponse{ + DiskAccessList: armcompute.DiskAccessList{ + Value: []*armcompute.DiskAccess{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.DiskAccessesClientListByResourceGroupResponse{ + DiskAccessList: armcompute.DiskAccessList{ + Value: []*armcompute.DiskAccess{item}, + }, + }, nil +} + +// errorDiskAccessesPager is a mock pager that always returns an error. +type errorDiskAccessesPager struct { + ctrl *gomock.Controller +} + +func newErrorDiskAccessesPager(ctrl *gomock.Controller) clients.DiskAccessesPager { + return &errorDiskAccessesPager{ctrl: ctrl} +} + +func (e *errorDiskAccessesPager) More() bool { + return true +} + +func (e *errorDiskAccessesPager) NextPage(ctx context.Context) (armcompute.DiskAccessesClientListByResourceGroupResponse, error) { + return armcompute.DiskAccessesClientListByResourceGroupResponse{}, errors.New("pager error") +} diff --git a/sources/azure/manual/compute-disk-encryption-set.go b/sources/azure/manual/compute-disk-encryption-set.go index c1d19b8b..08c6c811 100644 --- a/sources/azure/manual/compute-disk-encryption-set.go +++ b/sources/azure/manual/compute-disk-encryption-set.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -158,10 +158,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/locked down → DES can't access key material (In: true) - Out: false, // If DES is deleted/modified → Key Vault remains (Out: false) - }, }) } } @@ -192,10 +188,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/locked down → DES can't access key material (In: true) - Out: false, // If DES is deleted/modified → Key Vault remains (Out: false) - }, }) } } @@ -218,10 +210,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: keyQuery, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Key is deleted/modified → DES can't access key material (In: true) - Out: false, // If DES is deleted/modified → Key remains (Out: false) - }, }) } } @@ -235,11 +223,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -266,10 +249,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: keyQuery, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Key is deleted/modified → DES can't access key material (In: true) - Out: false, // If DES is deleted/modified → Key remains (Out: false) - }, }) } } @@ -284,11 +263,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -315,10 +289,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: identityName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If identity is deleted/permissions change → DES can't authenticate to Key Vault (In: true) - Out: false, // If DES is deleted/modified → identity remains (Out: false) - }, }) } } diff --git a/sources/azure/manual/compute-disk-encryption-set_test.go b/sources/azure/manual/compute-disk-encryption-set_test.go index f243fbf1..2052400f 100644 --- a/sources/azure/manual/compute-disk-encryption-set_test.go +++ b/sources/azure/manual/compute-disk-encryption-set_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -87,10 +87,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.ActiveKey.KeyURL - Key Vault Key @@ -98,10 +94,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vault", "test-key"), ExpectedScope: subscriptionID + "." + resourceGroup, // Key Vault URI doesn't contain resource group, use DES scope - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.ActiveKey.KeyURL - DNS name @@ -109,10 +101,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-vault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // Identity.UserAssignedIdentities[{id}] - User Assigned Identity @@ -120,10 +108,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -155,10 +139,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.ActiveKey.KeyURL - Key Vault Key @@ -166,10 +146,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vault", "test-key"), ExpectedScope: subscriptionID + "." + resourceGroup, // Key Vault URI doesn't contain resource group, use DES scope - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.ActiveKey.KeyURL - DNS name @@ -177,10 +153,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-vault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // Identity.UserAssignedIdentities[{id}] - User Assigned Identity @@ -188,10 +160,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.PreviousKeys[].SourceVault.ID - Key Vault Vault @@ -199,10 +167,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-old-vault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.PreviousKeys[].KeyURL - Key Vault Key @@ -210,10 +174,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-old-vault", "test-old-key"), ExpectedScope: subscriptionID + "." + resourceGroup, // Key Vault URI doesn't contain resource group, use DES scope - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.PreviousKeys[].KeyURL - DNS name @@ -221,10 +181,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-old-vault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/azure/manual/compute-disk.go b/sources/azure/manual/compute-disk.go index 79ba209b..ec8add3c 100644 --- a/sources/azure/manual/compute-disk.go +++ b/sources/azure/manual/compute-disk.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -138,10 +138,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vmName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted/modified → disk becomes detached (In: true) - Out: false, // If disk is deleted → VM remains but loses disk (Out: false) - }, }) } } @@ -164,10 +160,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vmName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted/modified → disk becomes detached (In: true) - Out: false, // If disk is deleted → VM remains but loses disk (Out: false) - }, }) } } @@ -192,10 +184,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vmName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted/modified → disk becomes detached (In: true) - Out: false, // If disk is deleted → VM remains but loses disk (Out: false) - }, }) } } @@ -218,10 +206,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: diskAccessName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Access is deleted/modified → disk private endpoint access is affected (In: true) - Out: false, // If disk is deleted → Disk Access remains (Out: false) - }, }) } } @@ -242,10 +226,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → disk encryption is affected (In: true) - Out: false, // If disk is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -266,10 +246,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → disk encryption is affected (In: true) - Out: false, // If disk is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -298,10 +274,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: diskName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source disk is deleted/modified → this disk may be affected (In: true) - Out: false, // If this disk is deleted → source disk remains (Out: false) - }, }) } } else if strings.Contains(sourceResourceID, "/snapshots/") { @@ -318,10 +290,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: snapshotName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source snapshot is deleted/modified → this disk may be affected (In: true) - Out: false, // If this disk is deleted → source snapshot remains (Out: false) - }, }) } } @@ -343,10 +311,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: storageAccountName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → disk import may fail (In: true) - Out: false, // If disk is deleted → Storage Account remains (Out: false) - }, }) } } @@ -370,10 +334,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: imageName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Image is deleted/modified → disk created from image may be affected (In: true) - Out: false, // If disk is deleted → Image remains (Out: false) - }, }) } } @@ -402,10 +362,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(galleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image is deleted/modified → disk created from image may be affected (In: true) - Out: false, // If disk is deleted → Gallery Image remains (Out: false) - }, }) } } @@ -430,10 +386,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(galleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image is deleted/modified → disk created from image may be affected (In: true) - Out: false, // If disk is deleted → Gallery Image remains (Out: false) - }, }) } } @@ -451,7 +403,7 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri allParts := strings.Split(strings.Trim(communityGalleryImageID, "/"), "/") communityGalleryName := "" for i, part := range allParts { - if part == "CommunityGalleries" && i+1 < len(allParts) { + if strings.EqualFold(part, "CommunityGalleries") && i+1 < len(allParts) { communityGalleryName = allParts[i+1] break } @@ -468,10 +420,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(communityGalleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Community Gallery Image is deleted/modified → disk created from image may be affected (In: true) - Out: false, // If disk is deleted → Community Gallery Image remains (Out: false) - }, }) } } @@ -501,10 +449,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(elasticSanName, volumeGroupName, snapshotName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Elastic SAN snapshot is deleted/modified → disk created from snapshot may be affected (In: true) - Out: false, // If disk is deleted → Elastic SAN snapshot remains (Out: false) - }, }) } } @@ -535,10 +479,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → disk encryption key access is affected (In: true) - Out: false, // If disk is deleted → Key Vault remains (Out: false) - }, }) } } @@ -557,10 +497,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(vaultName, secretName), Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Secret is deleted/modified → disk encryption key is affected (In: true) - Out: false, // If disk is deleted → Key Vault Secret remains (Out: false) - }, }) } @@ -574,11 +510,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -598,10 +529,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → key encryption key access is affected (In: true) - Out: false, // If disk is deleted → Key Vault remains (Out: false) - }, }) } } @@ -620,10 +547,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(vaultName, keyName), Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Key is deleted/modified → key encryption key is affected (In: true) - Out: false, // If disk is deleted → Key Vault Key remains (Out: false) - }, }) } @@ -637,11 +560,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/compute-disk_test.go b/sources/azure/manual/compute-disk_test.go index e610f146..8dd2c7f9 100644 --- a/sources/azure/manual/compute-disk_test.go +++ b/sources/azure/manual/compute-disk_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -90,210 +90,115 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // ManagedByExtended[0] - Virtual Machine ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm-2", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // ShareInfo[0].VMURI - Virtual Machine ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm-3", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.DiskAccessID - Disk Access ExpectedType: azureshared.ComputeDiskAccess.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-access", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.Encryption.DiskEncryptionSetID - Disk Encryption Set ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.SecurityProfile.SecureVMDiskEncryptionSetID - Disk Encryption Set ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-secure-vm-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.SourceResourceID (Disk) - Source Disk ExpectedType: azureshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-disk", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.StorageAccountID - Storage Account ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-storage-account", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.ImageReference.ID - Image ExpectedType: azureshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-image", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.GalleryImageReference.ID - Shared Gallery Image ExpectedType: azureshared.ComputeSharedGalleryImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-gallery", "test-gallery-image", "1.0.0"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.GalleryImageReference.SharedGalleryImageID - Shared Gallery Image ExpectedType: azureshared.ComputeSharedGalleryImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-gallery-2", "test-gallery-image-2", "2.0.0"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.GalleryImageReference.CommunityGalleryImageID - Community Gallery Image ExpectedType: azureshared.ComputeCommunityGalleryImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-community-gallery", "test-community-image", "1.0.0"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.ElasticSanResourceID - Elastic SAN Volume Snapshot ExpectedType: azureshared.ElasticSanVolumeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-elastic-san", "test-volume-group", "test-snapshot"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].DiskEncryptionKey.SourceVault.ID - Key Vault ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].DiskEncryptionKey.SecretURL - Key Vault Secret ExpectedType: azureshared.KeyVaultSecret.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-keyvault", "test-secret"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].DiskEncryptionKey.SecretURL - DNS name ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-keyvault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].KeyEncryptionKey.SourceVault.ID - Key Vault ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault-2", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].KeyEncryptionKey.KeyURL - Key Vault Key ExpectedType: azureshared.KeyVaultKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-keyvault-2", "test-key"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].KeyEncryptionKey.KeyURL - DNS name ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-keyvault-2.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -323,12 +228,6 @@ func TestComputeDisk(t *testing.T) { if linkedQuery.GetQuery().GetType() == azureshared.ComputeSnapshot.String() && linkedQuery.GetQuery().GetQuery() == "test-snapshot" { foundSnapshotLink = true - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected BlastPropagation.In to be true for snapshot link") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Errorf("Expected BlastPropagation.Out to be false for snapshot link") - } break } } diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go new file mode 100644 index 00000000..e5773d47 --- /dev/null +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -0,0 +1,389 @@ +package manual + +import ( + "context" + "errors" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ( + ComputeGalleryApplicationVersionLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplicationVersion) + ComputeGalleryApplicationLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplication) //todo: move to its adapter file when created, this is just a placeholder +) + +type computeGalleryApplicationVersionWrapper struct { + client clients.GalleryApplicationVersionsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeGalleryApplicationVersion(client clients.GalleryApplicationVersionsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &computeGalleryApplicationVersionWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeGalleryApplicationVersion, + ), + } +} + +func (c computeGalleryApplicationVersionWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 3 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 3 and be the gallery name, gallery application name, and gallery application version name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + galleryApplicationName := queryParts[1] + if galleryApplicationName == "" { + return nil, azureshared.QueryError(errors.New("gallery application name cannot be empty"), scope, c.Type()) + } + galleryApplicationVersionName := queryParts[2] + if galleryApplicationVersionName == "" { + return nil, azureshared.QueryError(errors.New("gallery application version name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + resp, err := c.client.Get(ctx, rgScope.ResourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureGalleryApplicationVersionToSDPItem(&resp.GalleryApplicationVersion, galleryName, galleryApplicationName, scope) +} + +func (c computeGalleryApplicationVersionWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 2 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 2 and be the gallery name and gallery application name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + galleryApplicationName := queryParts[1] + if galleryApplicationName == "" { + return nil, azureshared.QueryError(errors.New("gallery application name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByGalleryApplicationPager(rgScope.ResourceGroup, galleryName, galleryApplicationName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, galleryApplicationVersion := range page.Value { + if galleryApplicationVersion == nil || galleryApplicationVersion.Name == nil { + continue + } + item, sdpErr := c.azureGalleryApplicationVersionToSDPItem(galleryApplicationVersion, galleryName, galleryApplicationName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeGalleryApplicationVersionWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 2 { + stream.SendError(azureshared.QueryError(errors.New("queryParts must be exactly 2 and be the gallery name and gallery application name"), scope, c.Type())) + return + } + galleryName := queryParts[0] + if galleryName == "" { + stream.SendError(azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type())) + return + } + galleryApplicationName := queryParts[1] + if galleryApplicationName == "" { + stream.SendError(azureshared.QueryError(errors.New("gallery application name cannot be empty"), scope, c.Type())) + return + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByGalleryApplicationPager(rgScope.ResourceGroup, galleryName, galleryApplicationName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, galleryApplicationVersion := range page.Value { + if galleryApplicationVersion == nil || galleryApplicationVersion.Name == nil { + continue + } + item, sdpErr := c.azureGalleryApplicationVersionToSDPItem(galleryApplicationVersion, galleryName, galleryApplicationName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionToSDPItem( + galleryApplicationVersion *armcompute.GalleryApplicationVersion, + galleryName, + galleryApplicationName, + scope string, +) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(galleryApplicationVersion, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if galleryApplicationVersion.Name == nil { + return nil, azureshared.QueryError(errors.New("gallery application version name is nil"), scope, c.Type()) + } + galleryApplicationVersionName := *galleryApplicationVersion.Name + if galleryApplicationVersionName == "" { + return nil, azureshared.QueryError(errors.New("gallery application version name cannot be empty"), scope, c.Type()) + } + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent Gallery: version depends on gallery; deleting version does not delete gallery + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGallery.String(), + Method: sdp.QueryMethod_GET, + Query: galleryName, + Scope: scope, + }, + }) + + // Parent Gallery Application: version depends on application; deleting version does not delete application + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGalleryApplication.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(galleryName, galleryApplicationName), + Scope: scope, + }, + }) + + // MediaLink and DefaultConfigurationLink: add stdlib.NetworkHTTP, stdlib.NetworkDNS (hostname), stdlib.NetworkIP (when host is IP), azureshared.StorageAccount and azureshared.StorageBlobContainer (when Azure Blob) links. + // Dedupe DNS by hostname, IP by address, StorageAccount by account name, and StorageBlobContainer by (account, container) so the same resource is not linked twice. + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + seenStorageAccounts := make(map[string]struct{}) + seenBlobContainers := make(map[string]struct{}) + if galleryApplicationVersion.Properties != nil && galleryApplicationVersion.Properties.PublishingProfile != nil && galleryApplicationVersion.Properties.PublishingProfile.Source != nil { + src := galleryApplicationVersion.Properties.PublishingProfile.Source + addBlobLinks := func(link string) { + if link == "" || (!strings.HasPrefix(link, "http://") && !strings.HasPrefix(link, "https://")) { + return + } + AppendURILinks(&linkedItemQueries, link, linkedDNSHostnames, seenIPs, true, true) + if accountName := azureshared.ExtractStorageAccountNameFromBlobURI(link); accountName != "" { + if _, seen := seenStorageAccounts[accountName]; !seen { + seenStorageAccounts[accountName] = struct{}{} + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: accountName, + Scope: scope, + }, + }) + } + containerName := azureshared.ExtractContainerNameFromBlobURI(link) + if containerName != "" { + containerKey := shared.CompositeLookupKey(accountName, containerName) + if _, seen := seenBlobContainers[containerKey]; !seen { + seenBlobContainers[containerKey] = struct{}{} + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageBlobContainer.String(), + Method: sdp.QueryMethod_GET, + Query: containerKey, + Scope: scope, + }, + }) + } + } + } + } + if src.MediaLink != nil && *src.MediaLink != "" { + addBlobLinks(*src.MediaLink) + } + if src.DefaultConfigurationLink != nil && *src.DefaultConfigurationLink != "" { + defaultConfigLink := *src.DefaultConfigurationLink + if strings.HasPrefix(defaultConfigLink, "http://") || strings.HasPrefix(defaultConfigLink, "https://") { + sameAsMedia := src.MediaLink != nil && *src.MediaLink == defaultConfigLink + if !sameAsMedia { + addBlobLinks(defaultConfigLink) + } + } + } + } + + // Disk encryption sets from TargetRegions[].Encryption (OS and data disk); dedupe by ID + seenEncryptionSetIDs := make(map[string]struct{}) + if galleryApplicationVersion.Properties != nil && galleryApplicationVersion.Properties.PublishingProfile != nil && galleryApplicationVersion.Properties.PublishingProfile.TargetRegions != nil { + for _, tr := range galleryApplicationVersion.Properties.PublishingProfile.TargetRegions { + if tr == nil || tr.Encryption == nil { + continue + } + if tr.Encryption.OSDiskImage != nil && tr.Encryption.OSDiskImage.DiskEncryptionSetID != nil && *tr.Encryption.OSDiskImage.DiskEncryptionSetID != "" { + id := *tr.Encryption.OSDiskImage.DiskEncryptionSetID + if _, seen := seenEncryptionSetIDs[id]; !seen { + seenEncryptionSetIDs[id] = struct{}{} + name := azureshared.ExtractResourceName(id) + if name != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(id); extractedScope != "" { + linkScope = extractedScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: name, + Scope: linkScope, + }, + }) + } + } + } + if tr.Encryption.OSDiskImage != nil && tr.Encryption.OSDiskImage.SecurityProfile != nil && tr.Encryption.OSDiskImage.SecurityProfile.SecureVMDiskEncryptionSetID != nil && *tr.Encryption.OSDiskImage.SecurityProfile.SecureVMDiskEncryptionSetID != "" { + id := *tr.Encryption.OSDiskImage.SecurityProfile.SecureVMDiskEncryptionSetID + if _, seen := seenEncryptionSetIDs[id]; !seen { + seenEncryptionSetIDs[id] = struct{}{} + name := azureshared.ExtractResourceName(id) + if name != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(id); extractedScope != "" { + linkScope = extractedScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: name, + Scope: linkScope, + }, + }) + } + } + } + if tr.Encryption.DataDiskImages != nil { + for _, ddi := range tr.Encryption.DataDiskImages { + if ddi != nil && ddi.DiskEncryptionSetID != nil && *ddi.DiskEncryptionSetID != "" { + id := *ddi.DiskEncryptionSetID + if _, seen := seenEncryptionSetIDs[id]; !seen { + seenEncryptionSetIDs[id] = struct{}{} + name := azureshared.ExtractResourceName(id) + if name != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(id); extractedScope != "" { + linkScope = extractedScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: name, + Scope: linkScope, + }, + }) + } + } + } + } + } + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeGalleryApplicationVersion.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(galleryApplicationVersion.Tags), + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c computeGalleryApplicationVersionWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeGalleryLookupByName, + ComputeGalleryApplicationLookupByName, + ComputeGalleryApplicationVersionLookupByName, + } +} + +func (c computeGalleryApplicationVersionWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeGalleryLookupByName, + ComputeGalleryApplicationLookupByName, + }, + } +} + +func (c computeGalleryApplicationVersionWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeGallery, + azureshared.ComputeGalleryApplication, + azureshared.ComputeDiskEncryptionSet, + azureshared.StorageAccount, + azureshared.StorageBlobContainer, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/gallery_application_version +func (c computeGalleryApplicationVersionWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + //example id: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Compute/galleries/gallery1/applications/galleryApplication1/versions/galleryApplicationVersion1 + TerraformQueryMap: "azurerm_gallery_application_version.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeGalleryApplicationVersionWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/galleries/applications/versions/read", + } +} + +func (c computeGalleryApplicationVersionWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-gallery-application-version_test.go b/sources/azure/manual/compute-gallery-application-version_test.go new file mode 100644 index 00000000..25716e24 --- /dev/null +++ b/sources/azure/manual/compute-gallery-application-version_test.go @@ -0,0 +1,452 @@ +package manual + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +// mockGalleryApplicationVersionsPager is a mock pager for ListByGalleryApplication. +type mockGalleryApplicationVersionsPager struct { + pages []armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse + index int +} + +func (m *mockGalleryApplicationVersionsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockGalleryApplicationVersionsPager) NextPage(ctx context.Context) (armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse, error) { + if m.index >= len(m.pages) { + return armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +// errorGalleryApplicationVersionsPager is a mock pager that always returns an error. +type errorGalleryApplicationVersionsPager struct{} + +func (e *errorGalleryApplicationVersionsPager) More() bool { + return true +} + +func (e *errorGalleryApplicationVersionsPager) NextPage(ctx context.Context) (armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse, error) { + return armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse{}, errors.New("pager error") +} + +// testGalleryApplicationVersionsClient wraps the mock and returns a pager from NewListByGalleryApplicationPager. +type testGalleryApplicationVersionsClient struct { + *MockGalleryApplicationVersionsClient + pager clients.GalleryApplicationVersionsPager +} + +// NewListByGalleryApplicationPager returns the test pager so we don't need to mock this call. +func (t *testGalleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) clients.GalleryApplicationVersionsPager { + if t.pager != nil { + return t.pager + } + return t.MockGalleryApplicationVersionsClient.NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options) +} + +func createAzureGalleryApplicationVersion(versionName string) *armcompute.GalleryApplicationVersion { + return &armcompute.GalleryApplicationVersion{ + Name: to.Ptr(versionName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.GalleryApplicationVersionProperties{ + PublishingProfile: &armcompute.GalleryApplicationVersionPublishingProfile{ + Source: &armcompute.UserArtifactSource{ + MediaLink: to.Ptr("https://mystorageaccount.blob.core.windows.net/packages/app.zip"), + }, + }, + }, + } +} + +func createAzureGalleryApplicationVersionWithLinks(versionName, subscriptionID, resourceGroup string) *armcompute.GalleryApplicationVersion { + v := createAzureGalleryApplicationVersion(versionName) + v.Properties.PublishingProfile.Source.DefaultConfigurationLink = to.Ptr("https://mystorageaccount.blob.core.windows.net/config/default.json") + desID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-des" + v.Properties.PublishingProfile.TargetRegions = []*armcompute.TargetRegion{ + { + Name: to.Ptr("eastus"), + Encryption: &armcompute.EncryptionImages{ + OSDiskImage: &armcompute.OSDiskImageEncryption{ + DiskEncryptionSetID: to.Ptr(desID), + }, + }, + }, + } + return v +} + +func TestComputeGalleryApplicationVersion(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + galleryName := "test-gallery" + galleryApplicationName := "test-app" + galleryApplicationVersionName := "1.0.0" + + t.Run("Get", func(t *testing.T) { + version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{ + GalleryApplicationVersion: *version, + }, nil) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGalleryApplicationVersion.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeGalleryApplicationVersion.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag env=test, got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeGalleryApplication.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope}, + {ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "mystorageaccount", ExpectedScope: scope}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "packages"), ExpectedScope: scope}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/packages/app.zip", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "mystorageaccount.blob.core.windows.net", ExpectedScope: "global"}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithLinkedResources", func(t *testing.T) { + version := createAzureGalleryApplicationVersionWithLinks(galleryApplicationVersionName, subscriptionID, resourceGroup) + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{ + GalleryApplicationVersion: *version, + }, nil) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeGalleryApplication.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope}, + {ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "mystorageaccount", ExpectedScope: scope}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "packages"), ExpectedScope: scope}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "config"), ExpectedScope: scope}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/packages/app.zip", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "mystorageaccount.blob.core.windows.net", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/config/default.json", ExpectedScope: "global"}, + {ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-des", ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + // Adapter expects query to split into 3 parts (gallery, application, version); single part is invalid + _, qErr := adapter.Get(ctx, scope, galleryName, true) + if qErr == nil { + t.Error("Expected error when Get with wrong number of query parts, but got nil") + } + }) + + t.Run("Get_EmptyGalleryName", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", galleryApplicationName, galleryApplicationVersionName) + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when gallery name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("version not found") + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, "nonexistent", nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{}, expectedErr) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, "nonexistent") + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("Get_NonBlobURL_NoStorageLinks", func(t *testing.T) { + // MediaLink that is not Azure Blob Storage must not create StorageAccount/StorageBlobContainer links. + version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) + version.Properties.PublishingProfile.Source.MediaLink = to.Ptr("https://example.com/artifacts/app.zip") + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{ + GalleryApplicationVersion: *version, + }, nil) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query == nil { + continue + } + typ := query.GetType() + if typ == azureshared.StorageAccount.String() || typ == azureshared.StorageBlobContainer.String() { + t.Errorf("Non-blob URL must not create storage links; found linked query type %s with query %s", typ, query.GetQuery()) + } + } + // Should still have NetworkHTTP and NetworkDNS for the URL + hasHTTP := false + hasDNS := false + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query != nil { + if query.GetType() == stdlib.NetworkHTTP.String() { + hasHTTP = true + } + if query.GetType() == stdlib.NetworkDNS.String() { + hasDNS = true + } + } + } + if !hasHTTP { + t.Error("Expected NetworkHTTP linked query for the media URL") + } + if !hasDNS { + t.Error("Expected NetworkDNS linked query for the media URL hostname") + } + }) + + t.Run("Get_IPHost_EmitsIPLink", func(t *testing.T) { + // When MediaLink or DefaultConfigurationLink has a literal IP host, emit stdlib.NetworkIP link (GET, global), not DNS. + version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) + version.Properties.PublishingProfile.Source.MediaLink = to.Ptr("https://192.168.1.10:8443/artifacts/app.zip") + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{ + GalleryApplicationVersion: *version, + }, nil) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + hasIP := false + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query != nil && query.GetType() == stdlib.NetworkIP.String() { + hasIP = true + if query.GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected NetworkIP link to use GET, got %v", query.GetMethod()) + } + if query.GetScope() != "global" { + t.Errorf("Expected NetworkIP link scope global, got %s", query.GetScope()) + } + if query.GetQuery() != "192.168.1.10" { + t.Errorf("Expected NetworkIP link query 192.168.1.10, got %s", query.GetQuery()) + } + break + } + } + if !hasIP { + t.Error("Expected NetworkIP linked query when MediaLink host is an IP address") + } + }) + + t.Run("Search", func(t *testing.T) { + v1 := createAzureGalleryApplicationVersion("1.0.0") + v2 := createAzureGalleryApplicationVersion("1.0.1") + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + pages := []armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse{ + { + GalleryApplicationVersionList: armcompute.GalleryApplicationVersionList{ + Value: []*armcompute.GalleryApplicationVersion{v1, v2}, + }, + }, + } + mockPager := &mockGalleryApplicationVersionsPager{pages: pages} + testClient := &testGalleryApplicationVersionsClient{ + MockGalleryApplicationVersionsClient: mockClient, + pager: mockPager, + } + + wrapper := NewComputeGalleryApplicationVersion(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(galleryName, galleryApplicationName) + sdpItems, err := searchable.Search(ctx, scope, searchQuery, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if err := item.Validate(); err != nil { + t.Errorf("Expected valid item, got: %v", err) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + _, err := searchable.Search(ctx, scope, galleryName, true) + if err == nil { + t.Error("Expected error when Search with wrong number of query parts, but got nil") + } + }) + + t.Run("Search_EmptyGalleryName", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, scope, "", galleryApplicationName) + if qErr == nil { + t.Error("Expected error when gallery name is empty, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + errorPager := &errorGalleryApplicationVersionsPager{} + testClient := &testGalleryApplicationVersionsClient{ + MockGalleryApplicationVersionsClient: mockClient, + pager: errorPager, + } + + wrapper := NewComputeGalleryApplicationVersion(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(galleryName, galleryApplicationName) + _, err := searchable.Search(ctx, scope, searchQuery, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeGallery: true, + azureshared.ComputeGalleryApplication: true, + azureshared.ComputeDiskEncryptionSet: true, + azureshared.StorageAccount: true, + azureshared.StorageBlobContainer: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + stdlib.NetworkIP: true, + } + for itemType, want := range expected { + if got := links[itemType]; got != want { + t.Errorf("PotentialLinks()[%v] = %v, want %v", itemType, got, want) + } + } + }) + + t.Run("ImplementsSearchableAdapter", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("Adapter should implement SearchableAdapter interface") + } + }) +} diff --git a/sources/azure/manual/compute-gallery-image.go b/sources/azure/manual/compute-gallery-image.go new file mode 100644 index 00000000..8fdfd581 --- /dev/null +++ b/sources/azure/manual/compute-gallery-image.go @@ -0,0 +1,241 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ( + ComputeGalleryImageLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryImage) +) + +type computeGalleryImageWrapper struct { + client clients.GalleryImagesClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeGalleryImage(client clients.GalleryImagesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &computeGalleryImageWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeGalleryImage, + ), + } +} + +func (c computeGalleryImageWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 2 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 2 and be the gallery name and gallery image name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + galleryImageName := queryParts[1] + if galleryImageName == "" { + return nil, azureshared.QueryError(errors.New("gallery image name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + resp, err := c.client.Get(ctx, rgScope.ResourceGroup, galleryName, galleryImageName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureGalleryImageToSDPItem(&resp.GalleryImage, galleryName, scope) +} + +func (c computeGalleryImageWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the gallery name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByGalleryPager(rgScope.ResourceGroup, galleryName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, galleryImage := range page.Value { + if galleryImage == nil || galleryImage.Name == nil { + continue + } + item, sdpErr := c.azureGalleryImageToSDPItem(galleryImage, galleryName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeGalleryImageWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the gallery name"), scope, c.Type())) + return + } + galleryName := queryParts[0] + if galleryName == "" { + stream.SendError(azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type())) + return + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + + pager := c.client.NewListByGalleryPager(rgScope.ResourceGroup, galleryName, nil) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, galleryImage := range page.Value { + if galleryImage == nil || galleryImage.Name == nil { + continue + } + item, sdpErr := c.azureGalleryImageToSDPItem(galleryImage, galleryName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c computeGalleryImageWrapper) azureGalleryImageToSDPItem( + galleryImage *armcompute.GalleryImage, + galleryName, + scope string, +) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(galleryImage, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if galleryImage.Name == nil { + return nil, azureshared.QueryError(errors.New("gallery image name is nil"), scope, c.Type()) + } + galleryImageName := *galleryImage.Name + if galleryImageName == "" { + return nil, azureshared.QueryError(errors.New("gallery image name cannot be empty"), scope, c.Type()) + } + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(galleryName, galleryImageName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent Gallery: image definition depends on gallery + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGallery.String(), + Method: sdp.QueryMethod_GET, + Query: galleryName, + Scope: scope, + }, + }) + + // URI-based links: Eula, PrivacyStatementURI, ReleaseNoteURI + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + if galleryImage.Properties != nil { + if galleryImage.Properties.Eula != nil { + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.Eula, linkedDNSHostnames, seenIPs, true, false) + } + if galleryImage.Properties.PrivacyStatementURI != nil { + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs, true, false) + } + if galleryImage.Properties.ReleaseNoteURI != nil { + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.ReleaseNoteURI, linkedDNSHostnames, seenIPs, true, false) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeGalleryImage.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(galleryImage.Tags), + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c computeGalleryImageWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeGalleryLookupByName, + ComputeGalleryImageLookupByName, + } +} + +func (c computeGalleryImageWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeGalleryLookupByName, + }, + } +} + +func (c computeGalleryImageWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeGallery, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/shared_image +func (c computeGalleryImageWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + // example id: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Compute/galleries/gallery1/images/image1 + TerraformQueryMap: "azurerm_shared_image.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeGalleryImageWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/galleries/images/read", + } +} + +func (c computeGalleryImageWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-gallery-image_test.go b/sources/azure/manual/compute-gallery-image_test.go new file mode 100644 index 00000000..5a2cd299 --- /dev/null +++ b/sources/azure/manual/compute-gallery-image_test.go @@ -0,0 +1,466 @@ +package manual + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +// mockGalleryImagesPager is a mock pager for ListByGallery. +type mockGalleryImagesPager struct { + pages []armcompute.GalleryImagesClientListByGalleryResponse + index int +} + +func (m *mockGalleryImagesPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockGalleryImagesPager) NextPage(ctx context.Context) (armcompute.GalleryImagesClientListByGalleryResponse, error) { + if m.index >= len(m.pages) { + return armcompute.GalleryImagesClientListByGalleryResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +// errorGalleryImagesPager is a mock pager that always returns an error. +type errorGalleryImagesPager struct{} + +func (e *errorGalleryImagesPager) More() bool { + return true +} + +func (e *errorGalleryImagesPager) NextPage(ctx context.Context) (armcompute.GalleryImagesClientListByGalleryResponse, error) { + return armcompute.GalleryImagesClientListByGalleryResponse{}, errors.New("pager error") +} + +// testGalleryImagesClient wraps the mock and returns a pager from NewListByGalleryPager. +type testGalleryImagesClient struct { + *MockGalleryImagesClient + pager clients.GalleryImagesPager +} + +// NewListByGalleryPager returns the test pager so we don't need to mock this call. +func (t *testGalleryImagesClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) clients.GalleryImagesPager { + if t.pager != nil { + return t.pager + } + return t.MockGalleryImagesClient.NewListByGalleryPager(resourceGroupName, galleryName, options) +} + +func createAzureGalleryImage(imageName string) *armcompute.GalleryImage { + return &armcompute.GalleryImage{ + Name: to.Ptr(imageName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.GalleryImageProperties{ + Identifier: &armcompute.GalleryImageIdentifier{ + Publisher: to.Ptr("test-publisher"), + Offer: to.Ptr("test-offer"), + SKU: to.Ptr("test-sku"), + }, + OSType: to.Ptr(armcompute.OperatingSystemTypesLinux), + OSState: to.Ptr(armcompute.OperatingSystemStateTypesGeneralized), + }, + } +} + +func createAzureGalleryImageWithURIs(imageName string) *armcompute.GalleryImage { + img := createAzureGalleryImage(imageName) + img.Properties.Eula = to.Ptr("https://eula.example.com/terms") + img.Properties.PrivacyStatementURI = to.Ptr("https://example.com/privacy") + img.Properties.ReleaseNoteURI = to.Ptr("https://releases.example.com/notes") + return img +} + +func TestComputeGalleryImage(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + galleryName := "test-gallery" + galleryImageName := "test-image" + + t.Run("Get", func(t *testing.T) { + image := createAzureGalleryImage(galleryImageName) + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGalleryImage.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeGalleryImage.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(galleryName, galleryImageName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag env=test, got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithURIs", func(t *testing.T) { + image := createAzureGalleryImageWithURIs(galleryImageName) + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://eula.example.com/terms", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "eula.example.com", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/privacy", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://releases.example.com/notes", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "releases.example.com", ExpectedScope: "global"}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_PlainTextEula_NoLinks", func(t *testing.T) { + image := createAzureGalleryImage(galleryImageName) + image.Properties.Eula = to.Ptr("This software is provided as-is. No warranty.") + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + // Plain-text Eula should not generate HTTP/DNS/IP links + for _, q := range sdpItem.GetLinkedItemQueries() { + lq := q.GetQuery() + if lq == nil { + continue + } + typ := lq.GetType() + if typ == stdlib.NetworkHTTP.String() || typ == stdlib.NetworkDNS.String() || typ == stdlib.NetworkIP.String() { + t.Errorf("Plain-text Eula must not create network links; found linked query type %s with query %s", typ, lq.GetQuery()) + } + } + }) + + t.Run("Get_SameHostDeduplication", func(t *testing.T) { + image := createAzureGalleryImage(galleryImageName) + image.Properties.PrivacyStatementURI = to.Ptr("https://example.com/privacy") + image.Properties.ReleaseNoteURI = to.Ptr("https://example.com/release-notes") + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + // Should have 2 HTTP links (one per URI) but only 1 DNS link (same hostname) + httpCount := 0 + dnsCount := 0 + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query != nil { + if query.GetType() == stdlib.NetworkHTTP.String() { + httpCount++ + } + if query.GetType() == stdlib.NetworkDNS.String() { + dnsCount++ + } + } + } + if httpCount != 2 { + t.Errorf("Expected 2 HTTP links, got %d", httpCount) + } + if dnsCount != 1 { + t.Errorf("Expected 1 DNS link (deduped), got %d", dnsCount) + } + }) + + t.Run("Get_IPHost_EmitsIPLink", func(t *testing.T) { + image := createAzureGalleryImage(galleryImageName) + image.Properties.PrivacyStatementURI = to.Ptr("https://192.168.1.10:8443/privacy") + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + hasIP := false + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query != nil && query.GetType() == stdlib.NetworkIP.String() { + hasIP = true + if query.GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected NetworkIP link to use GET, got %v", query.GetMethod()) + } + if query.GetScope() != "global" { + t.Errorf("Expected NetworkIP link scope global, got %s", query.GetScope()) + } + if query.GetQuery() != "192.168.1.10" { + t.Errorf("Expected NetworkIP link query 192.168.1.10, got %s", query.GetQuery()) + } + break + } + } + if !hasIP { + t.Error("Expected NetworkIP linked query when PrivacyStatementURI host is an IP address") + } + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + // Adapter expects query to split into 2 parts (gallery, image); single part is invalid + _, qErr := adapter.Get(ctx, scope, galleryName, true) + if qErr == nil { + t.Error("Expected error when Get with wrong number of query parts, but got nil") + } + }) + + t.Run("Get_EmptyGalleryName", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", galleryImageName) + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when gallery name is empty, but got nil") + } + }) + + t.Run("Get_EmptyImageName", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, "") + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when image name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("image not found") + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, "nonexistent", nil).Return( + armcompute.GalleryImagesClientGetResponse{}, expectedErr) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, "nonexistent") + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + img1 := createAzureGalleryImage("image-1") + img2 := createAzureGalleryImage("image-2") + + mockClient := NewMockGalleryImagesClient(ctrl) + pages := []armcompute.GalleryImagesClientListByGalleryResponse{ + { + GalleryImageList: armcompute.GalleryImageList{ + Value: []*armcompute.GalleryImage{img1, img2}, + }, + }, + } + mockPager := &mockGalleryImagesPager{pages: pages} + testClient := &testGalleryImagesClient{ + MockGalleryImagesClient: mockClient, + pager: mockPager, + } + + wrapper := NewComputeGalleryImage(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + sdpItems, err := searchable.Search(ctx, scope, galleryName, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if err := item.Validate(); err != nil { + t.Errorf("Expected valid item, got: %v", err) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + // Search expects exactly 1 query part; giving 2 is invalid + searchQuery := shared.CompositeLookupKey(galleryName, galleryImageName) + _, err := searchable.Search(ctx, scope, searchQuery, true) + if err == nil { + t.Error("Expected error when Search with wrong number of query parts, but got nil") + } + }) + + t.Run("Search_EmptyGalleryName", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, scope, "") + if qErr == nil { + t.Error("Expected error when gallery name is empty, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + errorPager := &errorGalleryImagesPager{} + testClient := &testGalleryImagesClient{ + MockGalleryImagesClient: mockClient, + pager: errorPager, + } + + wrapper := NewComputeGalleryImage(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + _, err := searchable.Search(ctx, scope, galleryName, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeGallery: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + stdlib.NetworkIP: true, + } + for itemType, want := range expected { + if got := links[itemType]; got != want { + t.Errorf("PotentialLinks()[%v] = %v, want %v", itemType, got, want) + } + } + }) + + t.Run("ImplementsSearchableAdapter", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("Adapter should implement SearchableAdapter interface") + } + }) +} diff --git a/sources/azure/manual/compute-gallery.go b/sources/azure/manual/compute-gallery.go new file mode 100644 index 00000000..48f3fc40 --- /dev/null +++ b/sources/azure/manual/compute-gallery.go @@ -0,0 +1,211 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ComputeGalleryLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGallery) + +type computeGalleryWrapper struct { + client clients.GalleriesClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeGallery(client clients.GalleriesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeGalleryWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeGallery, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/galleries/list-by-resource-group +func (c computeGalleryWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, gallery := range page.Value { + if gallery == nil || gallery.Name == nil { + continue + } + item, sdpErr := c.azureGalleryToSDPItem(gallery, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeGalleryWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, gallery := range page.Value { + if gallery == nil || gallery.Name == nil { + continue + } + item, sdpErr := c.azureGalleryToSDPItem(gallery, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/galleries/get +func (c computeGalleryWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the gallery name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + resp, err := c.client.Get(ctx, rgScope.ResourceGroup, galleryName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureGalleryToSDPItem(&resp.Gallery, scope) +} + +func (c computeGalleryWrapper) azureGalleryToSDPItem(gallery *armcompute.Gallery, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(gallery, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if gallery.Name == nil { + return nil, azureshared.QueryError(errors.New("gallery name is nil"), scope, c.Type()) + } + galleryName := *gallery.Name + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Child resources: list gallery images under this gallery (Search by gallery name) + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGalleryImage.String(), + Method: sdp.QueryMethod_SEARCH, + Query: galleryName, + Scope: scope, + }, + }) + + // URI-based links from community gallery info: PublisherURI, Eula + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + if gallery.Properties != nil && gallery.Properties.SharingProfile != nil && gallery.Properties.SharingProfile.CommunityGalleryInfo != nil { + info := gallery.Properties.SharingProfile.CommunityGalleryInfo + if info.PublisherURI != nil { + AppendURILinks(&linkedItemQueries, *info.PublisherURI, linkedDNSHostnames, seenIPs, true, false) + } + if info.Eula != nil { + AppendURILinks(&linkedItemQueries, *info.Eula, linkedDNSHostnames, seenIPs, true, false) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeGallery.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(gallery.Tags), + LinkedItemQueries: linkedItemQueries, + } + + // Health status from ProvisioningState + if gallery.Properties != nil && gallery.Properties.ProvisioningState != nil { + switch *gallery.Properties.ProvisioningState { + case armcompute.GalleryProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armcompute.GalleryProvisioningStateCreating, armcompute.GalleryProvisioningStateUpdating, armcompute.GalleryProvisioningStateDeleting, armcompute.GalleryProvisioningStateMigrating: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armcompute.GalleryProvisioningStateFailed: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + return sdpItem, nil +} + +func (c computeGalleryWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeGalleryLookupByName, + } +} + +func (c computeGalleryWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeGalleryImage, + azureshared.ComputeGalleryApplication, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/shared_image_gallery +func (c computeGalleryWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_shared_image_gallery.name", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeGalleryWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/galleries/read", + } +} + +func (c computeGalleryWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-gallery_test.go b/sources/azure/manual/compute-gallery_test.go new file mode 100644 index 00000000..a6c2fde0 --- /dev/null +++ b/sources/azure/manual/compute-gallery_test.go @@ -0,0 +1,287 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestComputeGallery(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + + t.Run("Get", func(t *testing.T) { + galleryName := "test-gallery" + gallery := createAzureGallery(galleryName) + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, nil).Return( + armcompute.GalleriesClientGetResponse{ + Gallery: *gallery, + }, nil) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, galleryName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGallery.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeGallery.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != galleryName { + t.Errorf("Expected unique attribute value %s, got %s", galleryName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ComputeGalleryImage.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: galleryName, + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("List", func(t *testing.T) { + gallery1 := createAzureGallery("test-gallery-1") + gallery2 := createAzureGallery("test-gallery-2") + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockPager := newMockGalleriesPager(ctrl, []*armcompute.Gallery{gallery1, gallery2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + gallery1 := createAzureGallery("test-gallery-1") + gallery2 := createAzureGallery("test-gallery-2") + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockPager := newMockGalleriesPager(ctrl, []*armcompute.Gallery{gallery1, gallery2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + gallery1 := createAzureGallery("test-gallery-1") + galleryNilName := &armcompute.Gallery{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + } + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockPager := newMockGalleriesPager(ctrl, []*armcompute.Gallery{gallery1, galleryNilName}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("gallery not found") + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-gallery", nil).Return( + armcompute.GalleriesClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent-gallery", true) + if qErr == nil { + t.Error("Expected error when getting non-existent gallery, but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockGalleriesClient(ctrl) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting gallery with empty name, but got nil") + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockGalleriesClient(ctrl) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, scope) + if qErr == nil { + t.Error("Expected error when getting gallery with insufficient query parts, but got nil") + } + }) +} + +func createAzureGallery(galleryName string) *armcompute.Gallery { + return &armcompute.Gallery{ + Name: to.Ptr(galleryName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.GalleryProperties{ + Description: to.Ptr("Test shared image gallery"), + Identifier: &armcompute.GalleryIdentifier{ + UniqueName: to.Ptr("unique-" + galleryName), + }, + ProvisioningState: to.Ptr(armcompute.GalleryProvisioningStateSucceeded), + }, + } +} + +type mockGalleriesPager struct { + ctrl *gomock.Controller + items []*armcompute.Gallery + index int + more bool +} + +func newMockGalleriesPager(ctrl *gomock.Controller, items []*armcompute.Gallery) clients.GalleriesPager { + return &mockGalleriesPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockGalleriesPager) More() bool { + return m.more +} + +func (m *mockGalleriesPager) NextPage(ctx context.Context) (armcompute.GalleriesClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.GalleriesClientListByResourceGroupResponse{ + GalleryList: armcompute.GalleryList{ + Value: []*armcompute.Gallery{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.GalleriesClientListByResourceGroupResponse{ + GalleryList: armcompute.GalleryList{ + Value: []*armcompute.Gallery{item}, + }, + }, nil +} diff --git a/sources/azure/manual/compute-image.go b/sources/azure/manual/compute-image.go index 86bdf6c2..dc3975dd 100644 --- a/sources/azure/manual/compute-image.go +++ b/sources/azure/manual/compute-image.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -143,10 +143,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: diskName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source disk is deleted/modified → image becomes invalid (In: true) - Out: false, // If image is deleted → source disk remains (Out: false) - }, }) } } @@ -167,10 +163,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: snapshotName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source snapshot is deleted/modified → image becomes invalid (In: true) - Out: false, // If image is deleted → source snapshot remains (Out: false) - }, }) } } @@ -190,10 +182,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → image blob becomes inaccessible (In: true) - Out: false, // If image is deleted → Storage Account remains (Out: false) - }, }) } @@ -206,10 +194,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: blobURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → image cannot be accessed (In: true) - Out: true, // If image is deleted → HTTP endpoint may still be used by other resources (Out: true) - }, }) } @@ -223,11 +207,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -248,10 +227,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → image encryption is affected (In: true) - Out: false, // If image is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -280,10 +255,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: diskName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source disk is deleted/modified → image becomes invalid (In: true) - Out: false, // If image is deleted → source disk remains (Out: false) - }, }) } } @@ -304,10 +275,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: snapshotName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source snapshot is deleted/modified → image becomes invalid (In: true) - Out: false, // If image is deleted → source snapshot remains (Out: false) - }, }) } } @@ -325,10 +292,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → image blob becomes inaccessible (In: true) - Out: false, // If image is deleted → Storage Account remains (Out: false) - }, }) } @@ -341,10 +304,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: blobURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → image cannot be accessed (In: true) - Out: true, // If image is deleted → HTTP endpoint may still be used by other resources (Out: true) - }, }) } @@ -358,11 +317,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -383,10 +337,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → image encryption is affected (In: true) - Out: false, // If image is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -410,10 +360,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: vmName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source VM is deleted/modified → image source becomes invalid (In: true) - Out: false, // If image is deleted → source VM remains (Out: false) - }, }) } } diff --git a/sources/azure/manual/compute-image_test.go b/sources/azure/manual/compute-image_test.go index 3ee1cfd2..dd3a0609 100644 --- a/sources/azure/manual/compute-image_test.go +++ b/sources/azure/manual/compute-image_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -91,144 +91,79 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-os-disk", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // OSDisk.Snapshot.ID - Compute Snapshot ExpectedType: azureshared.ComputeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-os-snapshot", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // OSDisk.BlobURI - Storage Account ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "teststorageaccount", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // OSDisk.BlobURI - NetworkHTTP ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://teststorageaccount.blob.core.windows.net/vhds/osdisk.vhd", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // OSDisk.BlobURI - NetworkDNS ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "teststorageaccount.blob.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // OSDisk.DiskEncryptionSet.ID - Disk Encryption Set ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-os-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DataDisks[0].ManagedDisk.ID - Compute Disk ExpectedType: azureshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-data-disk-1", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DataDisks[0].Snapshot.ID - Compute Snapshot ExpectedType: azureshared.ComputeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-data-snapshot-1", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DataDisks[0].BlobURI - Storage Account ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "teststorageaccount2", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DataDisks[0].BlobURI - NetworkHTTP ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://teststorageaccount2.blob.core.windows.net/vhds/datadisk1.vhd", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DataDisks[0].BlobURI - NetworkDNS ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "teststorageaccount2.blob.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DataDisks[0].DiskEncryptionSet.ID - Disk Encryption Set ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-data-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // SourceVirtualMachine.ID - Virtual Machine ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-vm", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-proximity-placement-group.go b/sources/azure/manual/compute-proximity-placement-group.go index 42029628..c6684149 100644 --- a/sources/azure/manual/compute-proximity-placement-group.go +++ b/sources/azure/manual/compute-proximity-placement-group.go @@ -5,19 +5,19 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) - var ComputeProximityPlacementGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeProximityPlacementGroup) type computeProximityPlacementGroupWrapper struct { client clients.ProximityPlacementGroupsClient + *azureshared.MultiResourceGroupBase } @@ -138,10 +138,6 @@ func (c computeProximityPlacementGroupWrapper) azureProximityPlacementGroupToSDP Query: vmName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // PPG change affects VM placement - Out: true, // VM add/remove changes PPG membership - }, }) } } @@ -166,10 +162,6 @@ func (c computeProximityPlacementGroupWrapper) azureProximityPlacementGroupToSDP Query: avSetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // PPG change affects Availability Set placement - Out: true, // Availability Set add/remove changes PPG membership - }, }) } } @@ -194,10 +186,6 @@ func (c computeProximityPlacementGroupWrapper) azureProximityPlacementGroupToSDP Query: vmssName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // PPG change affects VMSS placement - Out: true, // VMSS add/remove changes PPG membership - }, }) } } diff --git a/sources/azure/manual/compute-proximity-placement-group_test.go b/sources/azure/manual/compute-proximity-placement-group_test.go index 23921bae..7e36dd32 100644 --- a/sources/azure/manual/compute-proximity-placement-group_test.go +++ b/sources/azure/manual/compute-proximity-placement-group_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -70,32 +70,17 @@ func TestComputeProximityPlacementGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.ComputeAvailabilitySet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-avset", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.ComputeVirtualMachineScaleSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vmss", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-shared-gallery-image.go b/sources/azure/manual/compute-shared-gallery-image.go new file mode 100644 index 00000000..37ba6c70 --- /dev/null +++ b/sources/azure/manual/compute-shared-gallery-image.go @@ -0,0 +1,234 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ( + ComputeSharedGalleryImageLookupByLocation = shared.NewItemTypeLookup("location", azureshared.ComputeSharedGalleryImage) + ComputeSharedGalleryImageLookupByGalleryUniqueName = shared.NewItemTypeLookup("galleryUniqueName", azureshared.ComputeSharedGalleryImage) + ComputeSharedGalleryImageLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeSharedGalleryImage) +) + +type computeSharedGalleryImageWrapper struct { + client clients.SharedGalleryImagesClient + *azureshared.SubscriptionBase +} + +func NewComputeSharedGalleryImage(client clients.SharedGalleryImagesClient, subscriptionID string) sources.SearchableWrapper { + return &computeSharedGalleryImageWrapper{ + client: client, + SubscriptionBase: azureshared.NewSubscriptionBase( + subscriptionID, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeSharedGalleryImage, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/shared-gallery-images/get +func (c computeSharedGalleryImageWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 3 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 3: location, gallery unique name, and image name"), scope, c.Type()) + } + location := queryParts[0] + if location == "" { + return nil, azureshared.QueryError(errors.New("location cannot be empty"), scope, c.Type()) + } + galleryUniqueName := queryParts[1] + if galleryUniqueName == "" { + return nil, azureshared.QueryError(errors.New("gallery unique name cannot be empty"), scope, c.Type()) + } + galleryImageName := queryParts[2] + if galleryImageName == "" { + return nil, azureshared.QueryError(errors.New("gallery image name cannot be empty"), scope, c.Type()) + } + + resp, err := c.client.Get(ctx, location, galleryUniqueName, galleryImageName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureSharedGalleryImageToSDPItem(&resp.SharedGalleryImage, location, galleryUniqueName, scope) +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/shared-gallery-images/list +func (c computeSharedGalleryImageWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 2 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 2: location and gallery unique name"), scope, c.Type()) + } + location := queryParts[0] + if location == "" { + return nil, azureshared.QueryError(errors.New("location cannot be empty"), scope, c.Type()) + } + galleryUniqueName := queryParts[1] + if galleryUniqueName == "" { + return nil, azureshared.QueryError(errors.New("gallery unique name cannot be empty"), scope, c.Type()) + } + + pager := c.client.NewListPager(location, galleryUniqueName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, image := range page.Value { + if image == nil || image.Name == nil { + continue + } + item, sdpErr := c.azureSharedGalleryImageToSDPItem(image, location, galleryUniqueName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeSharedGalleryImageWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 2 { + stream.SendError(azureshared.QueryError(errors.New("queryParts must be exactly 2: location and gallery unique name"), scope, c.Type())) + return + } + location := queryParts[0] + if location == "" { + stream.SendError(azureshared.QueryError(errors.New("location cannot be empty"), scope, c.Type())) + return + } + galleryUniqueName := queryParts[1] + if galleryUniqueName == "" { + stream.SendError(azureshared.QueryError(errors.New("gallery unique name cannot be empty"), scope, c.Type())) + return + } + + pager := c.client.NewListPager(location, galleryUniqueName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, image := range page.Value { + if image == nil || image.Name == nil { + continue + } + item, sdpErr := c.azureSharedGalleryImageToSDPItem(image, location, galleryUniqueName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c computeSharedGalleryImageWrapper) azureSharedGalleryImageToSDPItem( + image *armcompute.SharedGalleryImage, + location, + galleryUniqueName, + scope string, +) (*sdp.Item, *sdp.QueryError) { + if image.Name == nil { + return nil, azureshared.QueryError(errors.New("shared gallery image name is nil"), scope, c.Type()) + } + + attributes, err := shared.ToAttributesWithExclude(image) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + imageName := *image.Name + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(location, galleryUniqueName, imageName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent Shared Gallery: image definition depends on shared gallery (Microsoft.Compute/locations/sharedGalleries) + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSharedGallery.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(location, galleryUniqueName), + Scope: scope, + }, + }) + + // URI-based links. Note: armcompute.SharedGalleryImageProperties has no ReleaseNoteURI field (unlike GalleryImage). + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + if image.Properties != nil { + if image.Properties.Eula != nil { + AppendURILinks(&linkedItemQueries, *image.Properties.Eula, linkedDNSHostnames, seenIPs, true, false) + } + if image.Properties.PrivacyStatementURI != nil { + AppendURILinks(&linkedItemQueries, *image.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs, true, false) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeSharedGalleryImage.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c computeSharedGalleryImageWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeSharedGalleryImageLookupByLocation, + ComputeSharedGalleryImageLookupByGalleryUniqueName, + ComputeSharedGalleryImageLookupByName, + } +} + +func (c computeSharedGalleryImageWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeSharedGalleryImageLookupByLocation, + ComputeSharedGalleryImageLookupByGalleryUniqueName, + }, + } +} + +func (c computeSharedGalleryImageWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeSharedGallery, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// Shared gallery images are read-only views with no direct Terraform resource mapping. +func (c computeSharedGalleryImageWrapper) TerraformMappings() []*sdp.TerraformMapping { + return nil +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeSharedGalleryImageWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/locations/sharedGalleries/images/read", + } +} + +func (c computeSharedGalleryImageWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-shared-gallery-image_test.go b/sources/azure/manual/compute-shared-gallery-image_test.go new file mode 100644 index 00000000..25efb0a7 --- /dev/null +++ b/sources/azure/manual/compute-shared-gallery-image_test.go @@ -0,0 +1,464 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +func TestComputeSharedGalleryImage(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + location := "eastus" + galleryUniqueName := "test-gallery-unique-name" + imageName := "test-image" + + t.Run("Get", func(t *testing.T) { + image := createSharedGalleryImage(imageName) + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeSharedGalleryImage.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeSharedGalleryImage.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeSharedGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, galleryUniqueName), ExpectedScope: subscriptionID}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithURIs", func(t *testing.T) { + image := createSharedGalleryImageWithURIs(imageName) + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeSharedGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, galleryUniqueName), ExpectedScope: subscriptionID}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://eula.example.com/terms", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "eula.example.com", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/privacy", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global"}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_PlainTextEula_NoLinks", func(t *testing.T) { + image := createSharedGalleryImage(imageName) + image.Properties.Eula = to.Ptr("This software is provided as-is. No warranty.") + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + for _, q := range sdpItem.GetLinkedItemQueries() { + lq := q.GetQuery() + if lq == nil { + continue + } + typ := lq.GetType() + if typ == stdlib.NetworkHTTP.String() || typ == stdlib.NetworkDNS.String() || typ == stdlib.NetworkIP.String() { + t.Errorf("Plain-text Eula must not create network links; found linked query type %s with query %s", typ, lq.GetQuery()) + } + } + }) + + t.Run("Get_SameHostDeduplication", func(t *testing.T) { + image := createSharedGalleryImage(imageName) + image.Properties.Eula = to.Ptr("https://example.com/eula") + image.Properties.PrivacyStatementURI = to.Ptr("https://example.com/privacy") + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + httpCount := 0 + dnsCount := 0 + for _, q := range sdpItem.GetLinkedItemQueries() { + lq := q.GetQuery() + if lq != nil { + if lq.GetType() == stdlib.NetworkHTTP.String() { + httpCount++ + } + if lq.GetType() == stdlib.NetworkDNS.String() { + dnsCount++ + } + } + } + if httpCount != 2 { + t.Errorf("Expected 2 HTTP links, got %d", httpCount) + } + if dnsCount != 1 { + t.Errorf("Expected 1 DNS link (deduped), got %d", dnsCount) + } + }) + + t.Run("Get_IPHost_EmitsIPLink", func(t *testing.T) { + image := createSharedGalleryImage(imageName) + image.Properties.PrivacyStatementURI = to.Ptr("https://192.168.1.10:8443/privacy") + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + hasIP := false + for _, q := range sdpItem.GetLinkedItemQueries() { + lq := q.GetQuery() + if lq != nil && lq.GetType() == stdlib.NetworkIP.String() { + hasIP = true + if lq.GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected NetworkIP link to use GET, got %v", lq.GetMethod()) + } + if lq.GetScope() != "global" { + t.Errorf("Expected NetworkIP link scope global, got %s", lq.GetScope()) + } + if lq.GetQuery() != "192.168.1.10" { + t.Errorf("Expected NetworkIP link query 192.168.1.10, got %s", lq.GetQuery()) + } + break + } + } + if !hasIP { + t.Error("Expected NetworkIP linked query when PrivacyStatementURI host is an IP address") + } + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], location, true) + if qErr == nil { + t.Error("Expected error when Get with wrong number of query parts, but got nil") + } + }) + + t.Run("Get_EmptyLocation", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", galleryUniqueName, imageName) + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when location is empty, but got nil") + } + }) + + t.Run("Get_EmptyGalleryUniqueName", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, "", imageName) + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when gallery unique name is empty, but got nil") + } + }) + + t.Run("Get_EmptyImageName", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when image name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("image not found") + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, "nonexistent", nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, "nonexistent") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + img1 := createSharedGalleryImage("image-1") + img2 := createSharedGalleryImage("image-2") + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockPager := newMockSharedGalleryImagesPager([]*armcompute.SharedGalleryImage{img1, img2}) + mockClient.EXPECT().NewListPager(location, galleryUniqueName, nil).Return(mockPager) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(location, galleryUniqueName) + sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], searchQuery, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if err := item.Validate(); err != nil { + t.Errorf("Expected valid item, got: %v", err) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + _, err := searchable.Search(ctx, wrapper.Scopes()[0], searchQuery, true) + if err == nil { + t.Error("Expected error when Search with wrong number of query parts, but got nil") + } + }) + + t.Run("Search_EmptyLocation", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], "", galleryUniqueName) + if qErr == nil { + t.Error("Expected error when location is empty, but got nil") + } + }) + + t.Run("Search_EmptyGalleryUniqueName", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], location, "") + if qErr == nil { + t.Error("Expected error when gallery unique name is empty, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + errorPager := &errorSharedGalleryImagesPager{} + mockClient.EXPECT().NewListPager(location, galleryUniqueName, nil).Return(errorPager) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(location, galleryUniqueName) + _, err := searchable.Search(ctx, wrapper.Scopes()[0], searchQuery, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeSharedGallery: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + stdlib.NetworkIP: true, + } + for itemType, want := range expected { + if got := links[itemType]; got != want { + t.Errorf("PotentialLinks()[%v] = %v, want %v", itemType, got, want) + } + } + }) + + t.Run("ImplementsSearchableAdapter", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("Adapter should implement SearchableAdapter interface") + } + }) +} + +func createSharedGalleryImage(name string) *armcompute.SharedGalleryImage { + return &armcompute.SharedGalleryImage{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Identifier: &armcompute.SharedGalleryIdentifier{ + UniqueID: to.Ptr("/SharedGalleries/test-gallery-unique-name"), + }, + Properties: &armcompute.SharedGalleryImageProperties{ + Identifier: &armcompute.GalleryImageIdentifier{ + Publisher: to.Ptr("test-publisher"), + Offer: to.Ptr("test-offer"), + SKU: to.Ptr("test-sku"), + }, + OSType: to.Ptr(armcompute.OperatingSystemTypesLinux), + OSState: to.Ptr(armcompute.OperatingSystemStateTypesGeneralized), + }, + } +} + +func createSharedGalleryImageWithURIs(name string) *armcompute.SharedGalleryImage { + img := createSharedGalleryImage(name) + img.Properties.Eula = to.Ptr("https://eula.example.com/terms") + img.Properties.PrivacyStatementURI = to.Ptr("https://example.com/privacy") + return img +} + +type mockSharedGalleryImagesPager struct { + pages []armcompute.SharedGalleryImagesClientListResponse + index int +} + +func newMockSharedGalleryImagesPager(items []*armcompute.SharedGalleryImage) clients.SharedGalleryImagesPager { + return &mockSharedGalleryImagesPager{ + pages: []armcompute.SharedGalleryImagesClientListResponse{ + { + SharedGalleryImageList: armcompute.SharedGalleryImageList{ + Value: items, + }, + }, + }, + index: 0, + } +} + +func (m *mockSharedGalleryImagesPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockSharedGalleryImagesPager) NextPage(ctx context.Context) (armcompute.SharedGalleryImagesClientListResponse, error) { + if m.index >= len(m.pages) { + return armcompute.SharedGalleryImagesClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorSharedGalleryImagesPager struct{} + +func (e *errorSharedGalleryImagesPager) More() bool { + return true +} + +func (e *errorSharedGalleryImagesPager) NextPage(ctx context.Context) (armcompute.SharedGalleryImagesClientListResponse, error) { + return armcompute.SharedGalleryImagesClientListResponse{}, errors.New("pager error") +} diff --git a/sources/azure/manual/compute-snapshot.go b/sources/azure/manual/compute-snapshot.go new file mode 100644 index 00000000..80626272 --- /dev/null +++ b/sources/azure/manual/compute-snapshot.go @@ -0,0 +1,655 @@ +package manual + +import ( + "context" + "errors" + "net" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ComputeSnapshotLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeSnapshot) + +type computeSnapshotWrapper struct { + client clients.SnapshotsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeSnapshot(client clients.SnapshotsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeSnapshotWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_STORAGE, + azureshared.ComputeSnapshot, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/snapshots/list-by-resource-group +func (c computeSnapshotWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, snapshot := range page.Value { + if snapshot.Name == nil { + continue + } + item, sdpErr := c.azureSnapshotToSDPItem(snapshot, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeSnapshotWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, snapshot := range page.Value { + if snapshot.Name == nil { + continue + } + item, sdpErr := c.azureSnapshotToSDPItem(snapshot, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/snapshots/get +func (c computeSnapshotWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be at least 1 and be the snapshot name"), scope, c.Type()) + } + snapshotName := queryParts[0] + if snapshotName == "" { + return nil, azureshared.QueryError(errors.New("snapshotName cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + result, err := c.client.Get(ctx, rgScope.ResourceGroup, snapshotName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureSnapshotToSDPItem(&result.Snapshot, scope) +} + +func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snapshot, scope string) (*sdp.Item, *sdp.QueryError) { + if snapshot.Name == nil { + return nil, azureshared.QueryError(errors.New("snapshot name is nil"), scope, c.Type()) + } + attributes, err := shared.ToAttributesWithExclude(snapshot, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeSnapshot.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(snapshot.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Health status from ProvisioningState + if snapshot.Properties != nil && snapshot.Properties.ProvisioningState != nil { + switch *snapshot.Properties.ProvisioningState { + case "Succeeded": + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case "Creating", "Updating", "Deleting": + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case "Failed", "Canceled": + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Link to Disk Access from Properties.DiskAccessID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disk-accesses/get + if snapshot.Properties != nil && snapshot.Properties.DiskAccessID != nil && *snapshot.Properties.DiskAccessID != "" { + diskAccessName := azureshared.ExtractResourceName(*snapshot.Properties.DiskAccessID) + if diskAccessName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*snapshot.Properties.DiskAccessID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskAccess.String(), + Method: sdp.QueryMethod_GET, + Query: diskAccessName, + Scope: extractedScope, + }, + }) + } + } + + // Link to Disk Encryption Set from Properties.Encryption.DiskEncryptionSetID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disk-encryption-sets/get + if snapshot.Properties != nil && snapshot.Properties.Encryption != nil && snapshot.Properties.Encryption.DiskEncryptionSetID != nil && *snapshot.Properties.Encryption.DiskEncryptionSetID != "" { + encryptionSetName := azureshared.ExtractResourceName(*snapshot.Properties.Encryption.DiskEncryptionSetID) + if encryptionSetName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*snapshot.Properties.Encryption.DiskEncryptionSetID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: encryptionSetName, + Scope: extractedScope, + }, + }) + } + } + + // Link to Disk Encryption Set from Properties.SecurityProfile.SecureVMDiskEncryptionSetID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disk-encryption-sets/get + if snapshot.Properties != nil && snapshot.Properties.SecurityProfile != nil && snapshot.Properties.SecurityProfile.SecureVMDiskEncryptionSetID != nil && *snapshot.Properties.SecurityProfile.SecureVMDiskEncryptionSetID != "" { + encryptionSetName := azureshared.ExtractResourceName(*snapshot.Properties.SecurityProfile.SecureVMDiskEncryptionSetID) + if encryptionSetName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*snapshot.Properties.SecurityProfile.SecureVMDiskEncryptionSetID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: encryptionSetName, + Scope: extractedScope, + }, + }) + } + } + + // Link to source resources from Properties.CreationData + if snapshot.Properties != nil && snapshot.Properties.CreationData != nil { + creationData := snapshot.Properties.CreationData + + // Link to source Disk or Snapshot from SourceResourceID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disks/get + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/snapshots/get + if creationData.SourceResourceID != nil && *creationData.SourceResourceID != "" { + sourceResourceID := *creationData.SourceResourceID + sourceResourceIDLower := strings.ToLower(sourceResourceID) + if strings.Contains(sourceResourceIDLower, "/disks/") { + diskName := azureshared.ExtractResourceName(sourceResourceID) + if diskName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(sourceResourceID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDisk.String(), + Method: sdp.QueryMethod_GET, + Query: diskName, + Scope: extractedScope, + }, + }) + } + } else if strings.Contains(sourceResourceIDLower, "/snapshots/") { + snapshotName := azureshared.ExtractResourceName(sourceResourceID) + if snapshotName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(sourceResourceID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSnapshot.String(), + Method: sdp.QueryMethod_GET, + Query: snapshotName, + Scope: extractedScope, + }, + }) + } + } + } + + // Link to Storage Account from StorageAccountID + // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/get-properties + if creationData.StorageAccountID != nil && *creationData.StorageAccountID != "" { + storageAccountName := azureshared.ExtractResourceName(*creationData.StorageAccountID) + if storageAccountName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*creationData.StorageAccountID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: storageAccountName, + Scope: extractedScope, + }, + }) + } + } + + // Link to Storage Account and DNS from SourceURI (blob URI used for Import) + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/snapshots/create-or-update + if creationData.SourceURI != nil && *creationData.SourceURI != "" { + sourceURI := *creationData.SourceURI + storageAccountName := azureshared.ExtractStorageAccountNameFromBlobURI(sourceURI) + if storageAccountName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: storageAccountName, + Scope: scope, + }, + }) + + containerName := azureshared.ExtractContainerNameFromBlobURI(sourceURI) + if containerName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageBlobContainer.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(storageAccountName, containerName), + Scope: scope, + }, + }) + } + } + + if strings.HasPrefix(sourceURI, "http://") || strings.HasPrefix(sourceURI, "https://") { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: sourceURI, + Scope: "global", + }, + }) + } + + host := azureshared.ExtractDNSFromURL(sourceURI) + if host != "" { + if net.ParseIP(host) != nil { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: host, + Scope: "global", + }, + }) + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: host, + Scope: "global", + }, + }) + } + } + } + + // Link to Image from ImageReference.ID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/images/get + if creationData.ImageReference != nil && creationData.ImageReference.ID != nil && *creationData.ImageReference.ID != "" { + imageID := *creationData.ImageReference.ID + imageIDLower := strings.ToLower(imageID) + if strings.Contains(imageIDLower, "/images/") && !strings.Contains(imageIDLower, "/galleries/") { + imageName := azureshared.ExtractResourceName(imageID) + if imageName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(imageID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeImage.String(), + Method: sdp.QueryMethod_GET, + Query: imageName, + Scope: extractedScope, + }, + }) + } + } + } + + // Link to Gallery Image from GalleryImageReference + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/gallery-images/get + if creationData.GalleryImageReference != nil { + if creationData.GalleryImageReference.ID != nil && *creationData.GalleryImageReference.ID != "" { + galleryImageID := *creationData.GalleryImageReference.ID + parts := azureshared.ExtractPathParamsFromResourceID(galleryImageID, []string{"galleries", "images", "versions"}) + if len(parts) >= 3 { + galleryName := parts[0] + imageName := parts[1] + version := parts[2] + extractedScope := azureshared.ExtractScopeFromResourceID(galleryImageID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSharedGalleryImage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(galleryName, imageName, version), + Scope: extractedScope, + }, + }) + } + } + + if creationData.GalleryImageReference.SharedGalleryImageID != nil && *creationData.GalleryImageReference.SharedGalleryImageID != "" { + sharedGalleryImageID := *creationData.GalleryImageReference.SharedGalleryImageID + parts := azureshared.ExtractPathParamsFromResourceID(sharedGalleryImageID, []string{"galleries", "images", "versions"}) + if len(parts) >= 3 { + galleryName := parts[0] + imageName := parts[1] + version := parts[2] + extractedScope := azureshared.ExtractScopeFromResourceID(sharedGalleryImageID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSharedGalleryImage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(galleryName, imageName, version), + Scope: extractedScope, + }, + }) + } + } + + if creationData.GalleryImageReference.CommunityGalleryImageID != nil && *creationData.GalleryImageReference.CommunityGalleryImageID != "" { + communityGalleryImageID := *creationData.GalleryImageReference.CommunityGalleryImageID + parts := azureshared.ExtractPathParamsFromResourceID(communityGalleryImageID, []string{"Images", "Versions"}) + if len(parts) >= 2 { + imageName := parts[0] + version := parts[1] + allParts := strings.Split(strings.Trim(communityGalleryImageID, "/"), "/") + communityGalleryName := "" + for i, part := range allParts { + if strings.EqualFold(part, "CommunityGalleries") && i+1 < len(allParts) { + communityGalleryName = allParts[i+1] + break + } + } + if communityGalleryName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(communityGalleryImageID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeCommunityGalleryImage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(communityGalleryName, imageName, version), + Scope: extractedScope, + }, + }) + } + } + } + } + + // Link to Elastic SAN Volume Snapshot from ElasticSanResourceID + // Reference: https://learn.microsoft.com/en-us/rest/api/elasticsan/volume-snapshots/get + if creationData.ElasticSanResourceID != nil && *creationData.ElasticSanResourceID != "" { + elasticSanResourceID := *creationData.ElasticSanResourceID + parts := azureshared.ExtractPathParamsFromResourceID(elasticSanResourceID, []string{"elasticSans", "volumegroups", "snapshots"}) + if len(parts) >= 3 { + elasticSanName := parts[0] + volumeGroupName := parts[1] + esSnapshotName := parts[2] + extractedScope := azureshared.ExtractScopeFromResourceID(elasticSanResourceID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ElasticSanVolumeSnapshot.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(elasticSanName, volumeGroupName, esSnapshotName), + Scope: extractedScope, + }, + }) + } + } + } + + // Link to Key Vault resources from EncryptionSettingsCollection + // Reference: https://learn.microsoft.com/en-us/rest/api/keyvault/vaults/get + if snapshot.Properties != nil && snapshot.Properties.EncryptionSettingsCollection != nil && snapshot.Properties.EncryptionSettingsCollection.EncryptionSettings != nil { + for _, encryptionSetting := range snapshot.Properties.EncryptionSettingsCollection.EncryptionSettings { + if encryptionSetting == nil { + continue + } + + // Link to Key Vault from DiskEncryptionKey.SourceVault.ID + if encryptionSetting.DiskEncryptionKey != nil && encryptionSetting.DiskEncryptionKey.SourceVault != nil && encryptionSetting.DiskEncryptionKey.SourceVault.ID != nil && *encryptionSetting.DiskEncryptionKey.SourceVault.ID != "" { + vaultName := azureshared.ExtractResourceName(*encryptionSetting.DiskEncryptionKey.SourceVault.ID) + if vaultName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*encryptionSetting.DiskEncryptionKey.SourceVault.ID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultVault.String(), + Method: sdp.QueryMethod_GET, + Query: vaultName, + Scope: extractedScope, + }, + }) + } + } + + // Link to Key Vault Secret from DiskEncryptionKey.SecretURL + if encryptionSetting.DiskEncryptionKey != nil && encryptionSetting.DiskEncryptionKey.SecretURL != nil && *encryptionSetting.DiskEncryptionKey.SecretURL != "" { + secretURL := *encryptionSetting.DiskEncryptionKey.SecretURL + vaultName := azureshared.ExtractVaultNameFromURI(secretURL) + secretName := azureshared.ExtractSecretNameFromURI(secretURL) + if vaultName != "" && secretName != "" { + // Derive scope from the DiskEncryptionKey's SourceVault when available + secretScope := scope + if encryptionSetting.DiskEncryptionKey.SourceVault != nil && encryptionSetting.DiskEncryptionKey.SourceVault.ID != nil { + if extracted := azureshared.ExtractScopeFromResourceID(*encryptionSetting.DiskEncryptionKey.SourceVault.ID); extracted != "" { + secretScope = extracted + } + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultSecret.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(vaultName, secretName), + Scope: secretScope, + }, + }) + } + + secretHost := azureshared.ExtractDNSFromURL(secretURL) + if secretHost != "" { + if net.ParseIP(secretHost) != nil { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: secretHost, + Scope: "global", + }, + }) + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: secretHost, + Scope: "global", + }, + }) + } + } + } + + // Link to Key Vault from KeyEncryptionKey.SourceVault.ID + if encryptionSetting.KeyEncryptionKey != nil && encryptionSetting.KeyEncryptionKey.SourceVault != nil && encryptionSetting.KeyEncryptionKey.SourceVault.ID != nil && *encryptionSetting.KeyEncryptionKey.SourceVault.ID != "" { + vaultName := azureshared.ExtractResourceName(*encryptionSetting.KeyEncryptionKey.SourceVault.ID) + if vaultName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*encryptionSetting.KeyEncryptionKey.SourceVault.ID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultVault.String(), + Method: sdp.QueryMethod_GET, + Query: vaultName, + Scope: extractedScope, + }, + }) + } + } + + // Link to Key Vault Key from KeyEncryptionKey.KeyURL + if encryptionSetting.KeyEncryptionKey != nil && encryptionSetting.KeyEncryptionKey.KeyURL != nil && *encryptionSetting.KeyEncryptionKey.KeyURL != "" { + keyURL := *encryptionSetting.KeyEncryptionKey.KeyURL + vaultName := azureshared.ExtractVaultNameFromURI(keyURL) + keyName := azureshared.ExtractKeyNameFromURI(keyURL) + if vaultName != "" && keyName != "" { + // Derive scope from the KeyEncryptionKey's SourceVault when available + keyScope := scope + if encryptionSetting.KeyEncryptionKey.SourceVault != nil && encryptionSetting.KeyEncryptionKey.SourceVault.ID != nil { + if extracted := azureshared.ExtractScopeFromResourceID(*encryptionSetting.KeyEncryptionKey.SourceVault.ID); extracted != "" { + keyScope = extracted + } + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultKey.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(vaultName, keyName), + Scope: keyScope, + }, + }) + } + + keyHost := azureshared.ExtractDNSFromURL(keyURL) + if keyHost != "" { + if net.ParseIP(keyHost) != nil { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: keyHost, + Scope: "global", + }, + }) + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: keyHost, + Scope: "global", + }, + }) + } + } + } + } + } + + return sdpItem, nil +} + +func (c computeSnapshotWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeSnapshotLookupByName, + } +} + +func (c computeSnapshotWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeDisk, + azureshared.ComputeSnapshot, + azureshared.ComputeDiskAccess, + azureshared.ComputeDiskEncryptionSet, + azureshared.ComputeImage, + azureshared.ComputeSharedGalleryImage, + azureshared.ComputeCommunityGalleryImage, + azureshared.StorageAccount, + azureshared.StorageBlobContainer, + azureshared.ElasticSanVolumeSnapshot, + azureshared.KeyVaultVault, + azureshared.KeyVaultSecret, + azureshared.KeyVaultKey, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/snapshot +func (c computeSnapshotWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_snapshot.name", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeSnapshotWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/snapshots/read", + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/compute +func (c computeSnapshotWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-snapshot_test.go b/sources/azure/manual/compute-snapshot_test.go new file mode 100644 index 00000000..db9a434a --- /dev/null +++ b/sources/azure/manual/compute-snapshot_test.go @@ -0,0 +1,689 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +func TestComputeSnapshot(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + + t.Run("Get", func(t *testing.T) { + snapshotName := "test-snapshot" + snapshot := createAzureSnapshot(snapshotName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeSnapshot.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeSnapshot, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != snapshotName { + t.Errorf("Expected unique attribute value %s, got %s", snapshotName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + if sdpItem.GetHealth() != sdp.Health_HEALTH_OK { + t.Errorf("Expected health OK, got %s", sdpItem.GetHealth()) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Properties.DiskAccessID + ExpectedType: azureshared.ComputeDiskAccess.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-disk-access", + ExpectedScope: subscriptionID + "." + resourceGroup, }, + { + // Properties.Encryption.DiskEncryptionSetID + ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-des", + ExpectedScope: subscriptionID + "." + resourceGroup, }, + { + // Properties.CreationData.SourceResourceID (disk) + ExpectedType: azureshared.ComputeDisk.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "source-disk", + ExpectedScope: subscriptionID + "." + resourceGroup, }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithSnapshotSource", func(t *testing.T) { + snapshotName := "test-snapshot-from-snapshot" + snapshot := createAzureSnapshotFromSnapshot(snapshotName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Properties.CreationData.SourceResourceID (snapshot) + ExpectedType: azureshared.ComputeSnapshot.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "source-snapshot", + ExpectedScope: subscriptionID + "." + resourceGroup, }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithSourceURI", func(t *testing.T) { + snapshotName := "test-snapshot-from-blob" + snapshot := createAzureSnapshotFromBlobURI(snapshotName) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Properties.CreationData.SourceURI → Storage Account + ExpectedType: azureshared.StorageAccount.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "teststorageaccount", + ExpectedScope: subscriptionID + "." + resourceGroup, }, + { + // Properties.CreationData.SourceURI → Blob Container + ExpectedType: azureshared.StorageBlobContainer.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey("teststorageaccount", "vhds"), + ExpectedScope: subscriptionID + "." + resourceGroup, }, + { + // Properties.CreationData.SourceURI → HTTP + ExpectedType: stdlib.NetworkHTTP.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "https://teststorageaccount.blob.core.windows.net/vhds/my-disk.vhd", + ExpectedScope: "global", }, + { + // Properties.CreationData.SourceURI → DNS + ExpectedType: stdlib.NetworkDNS.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "teststorageaccount.blob.core.windows.net", + ExpectedScope: "global", }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithSourceURIUsingIPHost", func(t *testing.T) { + snapshotName := "test-snapshot-from-ip-blob" + snapshot := createAzureSnapshotFromIPBlobURI(snapshotName) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Properties.CreationData.SourceURI → HTTP + ExpectedType: stdlib.NetworkHTTP.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "https://10.0.0.1/vhds/my-disk.vhd", + ExpectedScope: "global", }, + { + // Properties.CreationData.SourceURI → IP (host is IP address) + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "10.0.0.1", + ExpectedScope: "global", }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + + // Verify no DNS link was emitted for the IP host + for _, link := range sdpItem.GetLinkedItemQueries() { + if link.GetQuery().GetType() == stdlib.NetworkDNS.String() { + t.Error("Expected no DNS link when SourceURI host is an IP address") + } + } + }) + + t.Run("GetWithEncryptionIPHosts", func(t *testing.T) { + snapshotName := "test-snapshot-encryption-ip" + snapshot := createAzureSnapshotWithEncryptionIPHosts(snapshotName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundSecretIPLink := false + foundKeyIPLink := false + for _, link := range sdpItem.GetLinkedItemQueries() { + if link.GetQuery().GetType() == stdlib.NetworkIP.String() { + if link.GetQuery().GetQuery() == "10.0.0.2" { + foundSecretIPLink = true + } + if link.GetQuery().GetQuery() == "10.0.0.3" { + foundKeyIPLink = true + } + if link.GetQuery().GetScope() != "global" { + t.Errorf("Expected IP scope 'global', got %s", link.GetQuery().GetScope()) + } + if link.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected IP method GET, got %s", link.GetQuery().GetMethod()) + } + } + if link.GetQuery().GetType() == stdlib.NetworkDNS.String() { + t.Error("Expected no DNS link when SecretURL/KeyURL hosts are IP addresses") + } + } + + if !foundSecretIPLink { + t.Error("Expected to find IP link for SecretURL host 10.0.0.2") + } + if !foundKeyIPLink { + t.Error("Expected to find IP link for KeyURL host 10.0.0.3") + } + }) + + t.Run("GetWithCrossResourceGroupLinks", func(t *testing.T) { + snapshotName := "test-snapshot-cross-rg" + snapshot := createAzureSnapshotWithCrossResourceGroupLinks(snapshotName, subscriptionID) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundDiskAccessLink := false + foundDiskLink := false + for _, link := range sdpItem.GetLinkedItemQueries() { + if link.GetQuery().GetType() == azureshared.ComputeDiskAccess.String() { + foundDiskAccessLink = true + expectedScope := subscriptionID + ".other-rg" + if link.GetQuery().GetScope() != expectedScope { + t.Errorf("Expected DiskAccess scope %s, got %s", expectedScope, link.GetQuery().GetScope()) + } + } + if link.GetQuery().GetType() == azureshared.ComputeDisk.String() { + foundDiskLink = true + expectedScope := subscriptionID + ".disk-rg" + if link.GetQuery().GetScope() != expectedScope { + t.Errorf("Expected Disk scope %s, got %s", expectedScope, link.GetQuery().GetScope()) + } + } + } + + if !foundDiskAccessLink { + t.Error("Expected to find Disk Access link") + } + if !foundDiskLink { + t.Error("Expected to find Disk link") + } + }) + + t.Run("GetWithoutLinks", func(t *testing.T) { + snapshotName := "test-snapshot-no-links" + snapshot := createAzureSnapshotWithoutLinks(snapshotName) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if len(sdpItem.GetLinkedItemQueries()) != 0 { + t.Errorf("Expected no linked queries, got %d", len(sdpItem.GetLinkedItemQueries())) + } + }) + + t.Run("List", func(t *testing.T) { + snapshot1 := createAzureSnapshot("test-snapshot-1", subscriptionID, resourceGroup) + snapshot2 := createAzureSnapshot("test-snapshot-2", subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockPager := newMockSnapshotsPager(ctrl, []*armcompute.Snapshot{snapshot1, snapshot2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, wrapper.Scopes()[0], true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + + if item.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %s", item.GetTags()["env"]) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + snapshot1 := createAzureSnapshot("test-snapshot-1", subscriptionID, resourceGroup) + snapshot2 := createAzureSnapshot("test-snapshot-2", subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockPager := newMockSnapshotsPager(ctrl, []*armcompute.Snapshot{snapshot1, snapshot2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, wrapper.Scopes()[0], true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + + // Verify adapter doesn't support SearchStream + _, ok = adapter.(discovery.SearchStreamableAdapter) + if ok { + t.Fatalf("Adapter should not support SearchStream operation") + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + snapshot1 := createAzureSnapshot("test-snapshot-1", subscriptionID, resourceGroup) + snapshotNilName := &armcompute.Snapshot{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + } + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockPager := newMockSnapshotsPager(ctrl, []*armcompute.Snapshot{snapshot1, snapshotNilName}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, wrapper.Scopes()[0], true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("snapshot not found") + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-snapshot", nil).Return( + armcompute.SnapshotsClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "nonexistent-snapshot", true) + if qErr == nil { + t.Error("Expected error when getting non-existent snapshot, but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockSnapshotsClient(ctrl) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "", true) + if qErr == nil { + t.Error("Expected error when getting snapshot with empty name, but got nil") + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSnapshotsClient(ctrl) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when getting snapshot with insufficient query parts, but got nil") + } + }) +} + +// createAzureSnapshot creates a mock Azure Snapshot with linked resources for testing +func createAzureSnapshot(name, subscriptionID, resourceGroup string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + DiskAccessID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskAccesses/test-disk-access"), + Encryption: &armcompute.Encryption{ + DiskEncryptionSetID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-des"), + }, + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), + SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/source-disk"), + }, + }, + } +} + +// createAzureSnapshotFromSnapshot creates a mock Snapshot that was copied from another snapshot +func createAzureSnapshotFromSnapshot(name, subscriptionID, resourceGroup string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), + SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/source-snapshot"), + }, + }, + } +} + +// createAzureSnapshotFromBlobURI creates a mock Snapshot imported from a blob URI +func createAzureSnapshotFromBlobURI(name string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionImport), + SourceURI: to.Ptr("https://teststorageaccount.blob.core.windows.net/vhds/my-disk.vhd"), + }, + }, + } +} + +// createAzureSnapshotFromIPBlobURI creates a mock Snapshot imported from a blob URI with an IP address host +func createAzureSnapshotFromIPBlobURI(name string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionImport), + SourceURI: to.Ptr("https://10.0.0.1/vhds/my-disk.vhd"), + }, + }, + } +} + +// createAzureSnapshotWithEncryptionIPHosts creates a mock Snapshot with encryption settings using IP-based SecretURL and KeyURL +func createAzureSnapshotWithEncryptionIPHosts(name, subscriptionID, resourceGroup string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty), + }, + EncryptionSettingsCollection: &armcompute.EncryptionSettingsCollection{ + Enabled: to.Ptr(true), + EncryptionSettings: []*armcompute.EncryptionSettingsElement{ + { + DiskEncryptionKey: &armcompute.KeyVaultAndSecretReference{ + SourceVault: &armcompute.SourceVault{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), + }, + SecretURL: to.Ptr("https://10.0.0.2/secrets/my-secret/version1"), + }, + KeyEncryptionKey: &armcompute.KeyVaultAndKeyReference{ + SourceVault: &armcompute.SourceVault{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), + }, + KeyURL: to.Ptr("https://10.0.0.3/keys/my-key/version1"), + }, + }, + }, + }, + }, + } +} + +// createAzureSnapshotWithCrossResourceGroupLinks creates a mock Snapshot with links to resources in different resource groups +func createAzureSnapshotWithCrossResourceGroupLinks(name, subscriptionID string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + DiskAccessID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/diskAccesses/test-disk-access"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), + SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/disk-rg/providers/Microsoft.Compute/disks/source-disk"), + }, + }, + } +} + +// createAzureSnapshotWithoutLinks creates a mock Snapshot without any linked resources +func createAzureSnapshotWithoutLinks(name string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty), + }, + }, + } +} + +// mockSnapshotsPager is a simple mock implementation of the Pager interface for testing +type mockSnapshotsPager struct { + ctrl *gomock.Controller + items []*armcompute.Snapshot + index int + more bool +} + +func newMockSnapshotsPager(ctrl *gomock.Controller, items []*armcompute.Snapshot) clients.SnapshotsPager { + return &mockSnapshotsPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockSnapshotsPager) More() bool { + return m.more +} + +func (m *mockSnapshotsPager) NextPage(ctx context.Context) (armcompute.SnapshotsClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.SnapshotsClientListByResourceGroupResponse{ + SnapshotList: armcompute.SnapshotList{ + Value: []*armcompute.Snapshot{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.SnapshotsClientListByResourceGroupResponse{ + SnapshotList: armcompute.SnapshotList{ + Value: []*armcompute.Snapshot{item}, + }, + }, nil +} diff --git a/sources/azure/manual/compute-virtual-machine-extension.go b/sources/azure/manual/compute-virtual-machine-extension.go index 5017576e..af59349a 100644 --- a/sources/azure/manual/compute-virtual-machine-extension.go +++ b/sources/azure/manual/compute-virtual-machine-extension.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -88,10 +90,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: virtualMachineName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted → Extension becomes invalid/unusable (In: true) - Out: false, // If Extension is deleted → VM remains functional (Out: false) - }, // Extension is a child resource of VM }) } @@ -115,10 +113,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → Extension settings access changes (In: true) - Out: false, // If Extension is deleted → Key Vault remains (Out: false) - }, // Extension depends on Key Vault for protected settings }) } } @@ -139,11 +133,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -166,10 +155,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS name is unavailable → Extension cannot resolve endpoint (In: true) - Out: true, // If Extension is deleted → DNS name may still be used by other resources (Out: true) - }, // Extension depends on DNS name for endpoint resolution }) } } @@ -195,10 +180,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS name is unavailable → Extension cannot resolve endpoint (In: true) - Out: true, // If Extension is deleted → DNS name may still be used by other resources (Out: true) - }, // Extension depends on DNS name for endpoint resolution }) } } @@ -249,6 +230,41 @@ func (c computeVirtualMachineExtensionWrapper) Search(ctx context.Context, scope return items, nil } +func (c computeVirtualMachineExtensionWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(fmt.Errorf("queryParts must be 1 query part: virtualMachineName, got %d", len(queryParts)), scope, c.Type())) + return + } + virtualMachineName := queryParts[0] + if virtualMachineName == "" { + stream.SendError(azureshared.QueryError(fmt.Errorf("virtualMachineName cannot be empty"), scope, c.Type())) + return + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + resp, err := c.client.List(ctx, rgScope.ResourceGroup, virtualMachineName, nil) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, extension := range resp.Value { + if extension.Name == nil { + continue + } + item, sdpErr := c.azureVirtualMachineExtensionToSDPItem(extension, virtualMachineName, *extension.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } +} + func (c computeVirtualMachineExtensionWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/compute-virtual-machine-extension_test.go b/sources/azure/manual/compute-virtual-machine-extension_test.go index c2cd9a87..e64c4a17 100644 --- a/sources/azure/manual/compute-virtual-machine-extension_test.go +++ b/sources/azure/manual/compute-virtual-machine-extension_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -70,12 +70,7 @@ func TestComputeVirtualMachineExtension(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: vmName, ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -121,12 +116,6 @@ func TestComputeVirtualMachineExtension(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected scope %s, got %s", scope, liq.GetQuery().GetScope()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected blast propagation In=true for Key Vault") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected blast propagation Out=false for Key Vault") - } case azureshared.ComputeVirtualMachine.String(): hasVMLink = true } @@ -188,9 +177,6 @@ func TestComputeVirtualMachineExtension(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { t.Errorf("Expected method SEARCH for DNS link, got %v", liq.GetQuery().GetMethod()) } - if liq.GetBlastPropagation().GetIn() != true || liq.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected blast propagation In: true, Out: true for DNS link, got In: %v, Out: %v", liq.GetBlastPropagation().GetIn(), liq.GetBlastPropagation().GetOut()) - } } } } @@ -403,17 +389,14 @@ func TestComputeVirtualMachineExtension(t *testing.T) { scope := subscriptionID + "." + resourceGroup - // Test with too few query parts + // Test with too few query parts (single segment - adapter rejects before calling wrapper) _, qErr := adapter.Get(ctx, scope, "only-vm-name", true) if qErr == nil { t.Error("Expected error for invalid query parts, got nil") } - - // Test with too many query parts - _, qErr = adapter.Get(ctx, scope, shared.CompositeLookupKey(vmName, extensionName, "extra"), true) - if qErr == nil { - t.Error("Expected error for too many query parts, got nil") - } + // Note: "too many" query parts are coalesced by the standard adapter (trailing segments + // merged into the last part), so the wrapper always receives exactly 2 parts and would + // call the client. We only test "too few" here to avoid requiring a mock Get expectation. }) t.Run("EmptyVirtualMachineName", func(t *testing.T) { diff --git a/sources/azure/manual/compute-virtual-machine-run-command.go b/sources/azure/manual/compute-virtual-machine-run-command.go index 62bede15..2ce81acf 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command.go +++ b/sources/azure/manual/compute-virtual-machine-run-command.go @@ -6,7 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -104,10 +106,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: virtualMachineName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted/modified → Run Command becomes invalid (In: true) - Out: false, // If Run Command is deleted → VM remains functional (Out: false) - }, // Run Command is a child resource of VM }) } @@ -139,10 +137,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: identityQuery, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Managed Identity is deleted/modified → Run Command cannot access blob/script (In: true) - Out: false, // If Run Command is deleted → Managed Identity remains (Out: false) - }, // Run Command depends on Managed Identity for blob/script access }) } @@ -153,53 +147,40 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS } uri := *blobURI - isBlobURI := strings.Contains(uri, ".blob.core.windows.net") - - // Check if it's an Azure blob URI (contains .blob.core.windows.net) - if isBlobURI { - storageAccountName := azureshared.ExtractStorageAccountNameFromBlobURI(uri) - if storageAccountName != "" { - // Link to Storage Account - // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/get-properties?view=rest-storagerp-2025-06-01&tabs=HTTP - // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}?api-version=2025-06-01 + storageAccountName := azureshared.ExtractStorageAccountNameFromBlobURI(uri) + if storageAccountName != "" { + // Link to Storage Account + // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/get-properties?view=rest-storagerp-2025-06-01&tabs=HTTP + // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}?api-version=2025-06-01 + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: storageAccountName, + Scope: scope, + }, + }) + + // Extract container name and link to Blob Container + containerName := azureshared.ExtractContainerNameFromBlobURI(uri) + if containerName != "" { + // Link to Blob Container + // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/blob-containers/get?view=rest-storagerp-2025-06-01&tabs=HTTP + // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default/containers/{containerName}?api-version=2025-06-01 sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: azureshared.StorageAccount.String(), + Type: azureshared.StorageBlobContainer.String(), Method: sdp.QueryMethod_GET, - Query: storageAccountName, + Query: shared.CompositeLookupKey(storageAccountName, containerName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → blob becomes inaccessible (In: true) - Out: false, // If Run Command is deleted → Storage Account remains (Out: false) - }, // Run Command depends on Storage Account for blob access }) - - // Extract container name and link to Blob Container - containerName := azureshared.ExtractContainerNameFromBlobURI(uri) - if containerName != "" { - // Link to Blob Container - // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/blob-containers/get?view=rest-storagerp-2025-06-01&tabs=HTTP - // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default/containers/{containerName}?api-version=2025-06-01 - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.StorageBlobContainer.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(storageAccountName, containerName), - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Blob Container is deleted/modified → blob becomes inaccessible (In: true) - Out: false, // If Run Command is deleted → Blob Container remains (Out: false) - }, // Run Command depends on Blob Container for blob access - }) - } } } // Link to stdlib.NetworkHTTP and DNS only for non-blob URIs // For blob URIs, the StorageBlobContainer already has these links - if !isBlobURI && (strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://")) { + if storageAccountName == "" && (strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://")) { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ Type: stdlib.NetworkHTTP.String(), @@ -207,10 +188,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: uri, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → Run Command cannot access script/blob (In: true) - Out: true, // If Run Command is deleted → HTTP endpoint may still be used by other resources (Out: true) - }, // Run Command depends on HTTP endpoint for script/blob access }) // Link to DNS name (standard library) from URI @@ -223,10 +200,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS name is unavailable → Run Command cannot resolve endpoint (In: true) - Out: true, // If Run Command is deleted → DNS name may still be used by other resources (Out: true) - }, // Run Command depends on DNS name for endpoint resolution }) } } @@ -310,6 +283,44 @@ func (s computeVirtualMachineRunCommandWrapper) Search(ctx context.Context, scop return items, nil } +func (s computeVirtualMachineRunCommandWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(errors.New("search requires exactly 1 query part: virtualMachineName"), scope, s.Type())) + return + } + virtualMachineName := queryParts[0] + if virtualMachineName == "" { + stream.SendError(azureshared.QueryError(errors.New("virtualMachineName cannot be empty"), scope, s.Type())) + return + } + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.NewListByVirtualMachinePager(rgScope.ResourceGroup, virtualMachineName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, runCommand := range page.Value { + if runCommand.Name == nil { + continue + } + item, sdpErr := s.azureVirtualMachineRunCommandToSDPItem(runCommand, virtualMachineName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s computeVirtualMachineRunCommandWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/compute-virtual-machine-run-command_test.go b/sources/azure/manual/compute-virtual-machine-run-command_test.go index e33ef6ac..fa0ddc92 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command_test.go +++ b/sources/azure/manual/compute-virtual-machine-run-command_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -150,12 +150,7 @@ func TestComputeVirtualMachineRunCommand(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: vmName, ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -203,12 +198,6 @@ func TestComputeVirtualMachineRunCommand(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected method GET, got %s", liq.GetQuery().GetMethod()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected blast propagation In=true for Storage Account") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected blast propagation Out=false for Storage Account") - } case azureshared.StorageBlobContainer.String(): blobContainerLinks++ expectedQuery := shared.CompositeLookupKey("mystorageaccount", "outputcontainer") diff --git a/sources/azure/manual/compute-virtual-machine-scale-set.go b/sources/azure/manual/compute-virtual-machine-scale-set.go index 44b2354e..056294bd 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -150,10 +150,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(scaleSetName, *extension.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // If Extensions are deleted → VMSS remains functional (In: false) - Out: true, // If VMSS is deleted → Extensions become invalid/unusable (Out: true) - }, }) } } @@ -170,10 +166,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: scaleSetName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // If VM instances are deleted → VMSS remains functional (In: false) - Out: true, // If VMSS is deleted → VM instances become invalid/unusable (Out: true) - }, }) } @@ -200,10 +192,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: nsgName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NSG changes → VMSS network behavior changes (In: true) - Out: false, // If VMSS is deleted → NSG remains (Out: false) - }, }) } } @@ -236,10 +224,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: vnetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Virtual Network changes → VMSS network behavior changes (In: true) - Out: false, // If VMSS is deleted → Virtual Network remains (Out: false) - }, }) // Link to Subnet sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -249,10 +233,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Subnet changes → VMSS network behavior changes (In: true) - Out: false, // If VMSS is deleted → Subnet remains (Out: false) - }, }) } } @@ -277,10 +257,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: publicIPPrefixName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Public IP Prefix changes → VMSS public IP allocation changes (In: true) - Out: false, // If VMSS is deleted → Public IP Prefix remains (Out: false) - }, }) } } @@ -311,10 +287,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: lbName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Load Balancer changes → VMSS load balancing changes (In: true) - Out: false, // If VMSS is deleted → Load Balancer remains (Out: false) - }, }) // Link to Backend Address Pool sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -324,10 +296,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(lbName, poolName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Backend Pool changes → VMSS load balancing changes (In: true) - Out: true, // If VMSS is deleted → Backend Pool loses members (Out: true) - }, }) } } @@ -360,10 +328,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: lbName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Load Balancer changes → VMSS load balancing changes (In: true) - Out: false, // If VMSS is deleted → Load Balancer remains (Out: false) - }, }) // Link to Inbound NAT Pool sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -373,10 +337,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(lbName, poolName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NAT Pool changes → VMSS NAT behavior changes (In: true) - Out: true, // If VMSS is deleted → NAT Pool loses members (Out: true) - }, }) } } @@ -409,10 +369,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: agName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Application Gateway changes → VMSS routing changes (In: true) - Out: false, // If VMSS is deleted → Application Gateway remains (Out: false) - }, }) // Link to Backend Address Pool sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -422,10 +378,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(agName, poolName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Backend Pool changes → VMSS routing changes (In: true) - Out: true, // If VMSS is deleted → Backend Pool loses members (Out: true) - }, }) } } @@ -451,10 +403,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: asgName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If ASG changes → VMSS network rules change (In: true) - Out: false, // If VMSS is deleted → ASG remains (Out: false) - }, }) } } @@ -495,10 +443,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: lbName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Load Balancer changes → VMSS load balancing changes (In: true) - Out: false, // If VMSS is deleted → Load Balancer remains (Out: false) - }, }) // Link to Health Probe sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -508,10 +452,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(lbName, probeName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Health Probe changes → VMSS health checks change (In: true) - Out: false, // If VMSS is deleted → Health Probe remains (Out: false) - }, }) } } @@ -539,10 +479,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set changes → VMSS disk encryption changes (In: true) - Out: false, // If VMSS is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -566,10 +502,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set changes → VMSS disk encryption changes (In: true) - Out: false, // If VMSS is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -603,10 +535,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: imageName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Image changes → VMSS VM configuration changes (In: true) - Out: false, // If VMSS is deleted → Image remains (Out: false) - }, }) } } @@ -635,10 +563,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(galleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image changes → VMSS VM configuration changes (In: true) - Out: false, // If VMSS is deleted → Gallery Image remains (Out: false) - }, }) } } @@ -664,10 +588,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(communityGalleryName, imageName, version), Scope: scope, // Community galleries are subscription-level }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image changes → VMSS VM configuration changes (In: true) - Out: false, // If VMSS is deleted → Gallery Image remains (Out: false) - }, }) } } @@ -698,15 +618,11 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt } sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: azureshared.ComputeSharedGalleryApplicationVersion.String(), + Type: azureshared.ComputeGalleryApplicationVersion.String(), Method: sdp.QueryMethod_GET, Query: shared.CompositeLookupKey(galleryName, applicationName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Application Version changes → VMSS application configuration changes (In: true) - Out: false, // If VMSS is deleted → Gallery Application Version remains (Out: false) - }, }) } } @@ -733,10 +649,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: ppgName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If PPG changes → VMSS placement changes (In: true) - Out: false, // If VMSS is deleted → PPG remains (Out: false) - }, }) } } @@ -758,10 +670,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: hostGroupName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Host Group changes → VMSS host placement changes (In: true) - Out: false, // If VMSS is deleted → Host Group remains (Out: false) - }, }) } } @@ -787,10 +695,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: capacityReservationGroupName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Capacity Reservation Group changes → VMSS capacity reservation changes (In: true) - Out: false, // If VMSS is deleted → Capacity Reservation Group remains (Out: false) - }, }) } } @@ -814,10 +718,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: identityName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Identity changes → VMSS access changes (In: true) - Out: false, // If VMSS is deleted → Identity remains (Out: false) - }, }) } } @@ -846,11 +746,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) // Extract account name (everything before the first dot) @@ -871,10 +766,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account changes → VMSS boot diagnostics affected (In: true) - Out: false, // If VMSS is deleted → Storage Account remains (Out: false) - }, }) } } @@ -902,10 +793,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → VMSS secrets access changes (In: true) - Out: false, // If VMSS is deleted → Key Vault remains (Out: false) - }, }) } } @@ -934,10 +821,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → VMSS extension settings access changes (In: true) - Out: false, // If VMSS is deleted → Key Vault remains (Out: false) - }, }) } } @@ -990,7 +873,7 @@ func (c computeVirtualMachineScaleSetWrapper) PotentialLinks() map[shared.ItemTy azureshared.ComputeImage, azureshared.ComputeSharedGalleryImage, azureshared.ComputeCommunityGalleryImage, - azureshared.ComputeSharedGalleryApplicationVersion, + azureshared.ComputeGalleryApplicationVersion, // Storage resources azureshared.StorageAccount, // Identity resources diff --git a/sources/azure/manual/compute-virtual-machine-scale-set_test.go b/sources/azure/manual/compute-virtual-machine-scale-set_test.go index fbdc03e9..62729dfa 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -75,254 +75,139 @@ func TestComputeVirtualMachineScaleSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(scaleSetName, "CustomScriptExtension"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // VM instances - always linked via SEARCH ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: scaleSetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Network Security Group ExpectedType: azureshared.NetworkNetworkSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nsg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Virtual Network ExpectedType: azureshared.NetworkVirtualNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vnet", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet - uses composite lookup key ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "default"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Public IP Prefix ExpectedType: azureshared.NetworkPublicIPPrefix.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pip-prefix", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Load Balancer ExpectedType: azureshared.NetworkLoadBalancer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-lb", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Load Balancer Backend Address Pool - uses composite lookup key ExpectedType: azureshared.NetworkLoadBalancerBackendAddressPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-lb", "test-backend-pool"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Load Balancer Inbound NAT Pool - uses composite lookup key ExpectedType: azureshared.NetworkLoadBalancerInboundNatPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-lb", "test-nat-pool"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Application Gateway ExpectedType: azureshared.NetworkApplicationGateway.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-ag", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Application Gateway Backend Address Pool - uses composite lookup key ExpectedType: azureshared.NetworkApplicationGatewayBackendAddressPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-ag", "test-ag-pool"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Application Security Group ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-asg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Load Balancer Health Probe - uses composite lookup key ExpectedType: azureshared.NetworkLoadBalancerProbe.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-lb", "test-probe"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Disk Encryption Set (OS Disk) ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Disk Encryption Set (Data Disk) ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-encryption-set-data", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Image (custom image) ExpectedType: azureshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-image", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Proximity Placement Group ExpectedType: azureshared.ComputeProximityPlacementGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-ppg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Dedicated Host Group ExpectedType: azureshared.ComputeDedicatedHostGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-host-group", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User Assigned Identity ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS name (boot diagnostics storage URI) ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "teststorageaccount.blob.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Storage Account (boot diagnostics) ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "teststorageaccount", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Key Vault (OS profile secrets) ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Key Vault (extension protected settings) ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault-ext", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-virtual-machine.go b/sources/azure/manual/compute-virtual-machine.go index 5e5f78e9..03350d33 100644 --- a/sources/azure/manual/compute-virtual-machine.go +++ b/sources/azure/manual/compute-virtual-machine.go @@ -6,9 +6,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -59,7 +59,7 @@ func (c computeVirtualMachineWrapper) PotentialLinks() map[shared.ItemType]bool azureshared.ComputeVirtualMachineScaleSet, azureshared.ComputeImage, azureshared.ComputeSharedGalleryImage, - azureshared.ComputeSharedGalleryApplicationVersion, + azureshared.ComputeGalleryApplicationVersion, azureshared.ComputeVirtualMachineExtension, azureshared.ComputeVirtualMachineRunCommand, azureshared.ManagedIdentityUserAssignedIdentity, @@ -222,10 +222,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: diskName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If disk changes → VM affected (In: true) - Out: true, // If VM is deleted → disk may be deleted depending on delete option (Out: true) - }, }) } // Link to disk encryption set for OS disk @@ -244,10 +240,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: diskEncryptionSetName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If encryption set changes → disk encryption affected (In: true) - Out: false, // If VM is deleted → encryption set remains (Out: false) - }, }) } } @@ -272,10 +264,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: diskName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If disk changes → VM affected (In: true) - Out: true, // If VM is deleted → disk may be deleted depending on delete option (Out: true) - }, }) } // Link to disk encryption set for data disk @@ -294,10 +282,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: diskEncryptionSetName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If encryption set changes → disk encryption affected (In: true) - Out: false, // If VM is deleted → encryption set remains (Out: false) - }, }) } } @@ -324,10 +308,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: nicName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NIC changes → VM network connectivity affected (In: true) - Out: false, // If VM is deleted → NIC remains (Out: false) - }, }) } } @@ -351,10 +331,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: availabilitySetName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If availability set changes → VM placement affected (In: true) - Out: false, // If VM is deleted → availability set remains (Out: false) - }, }) } } @@ -375,10 +351,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: proximityPlacementGroupName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If proximity placement group changes → VM placement affected (In: true) - Out: false, // If VM is deleted → proximity placement group remains (Out: false) - }, }) } } @@ -399,10 +371,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: hostGroupName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If host group changes → VM host placement affected (In: true) - Out: false, // If VM is deleted → host group remains (Out: false) - }, }) } } @@ -423,10 +391,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: capacityReservationGroupName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If capacity reservation group changes → VM capacity reservation affected (In: true) - Out: false, // If VM is deleted → capacity reservation group remains (Out: false) - }, }) } } @@ -447,10 +411,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vmssName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VMSS changes → VM configuration affected (In: true) - Out: false, // If VM is deleted → VMSS remains (Out: false) - }, }) } } @@ -473,10 +433,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vmssName[0], Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VMSS changes → VM configuration affected (In: true) - Out: false, // If VM is deleted → VMSS remains (Out: false) - }, }) } } @@ -507,10 +463,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: shared.CompositeLookupKey(galleryName, imageName, versionName), Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If image version changes → VM image affected (In: true) - Out: false, // If VM is deleted → image version remains (Out: false) - }, }) } } else if strings.Contains(imageID, "/images/") { @@ -528,10 +480,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: imageName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If image changes → VM image affected (In: true) - Out: false, // If VM is deleted → image remains (Out: false) - }, }) } } @@ -555,10 +503,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: identityName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If identity changes → VM identity access affected (In: true) - Out: false, // If VM is deleted → identity remains (Out: false) - }, }) } } @@ -582,10 +526,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vaultName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → VM secrets access affected (In: true) - Out: false, // If VM is deleted → Key Vault remains (Out: false) - }, }) } } @@ -612,10 +552,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vaultName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → disk encryption affected (In: true) - Out: false, // If VM is deleted → Key Vault remains (Out: false) - }, }) } } @@ -634,10 +570,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vaultName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → disk encryption affected (In: true) - Out: false, // If VM is deleted → Key Vault remains (Out: false) - }, }) } } @@ -662,15 +594,11 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput } sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: azureshared.ComputeSharedGalleryApplicationVersion.String(), + Type: azureshared.ComputeGalleryApplicationVersion.String(), Method: sdp.QueryMethod_GET, Query: shared.CompositeLookupKey(galleryName, appName, versionName), Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If application version changes → VM application affected (In: true) - Out: false, // If VM is deleted → application version remains (Out: false) - }, }) } } @@ -690,10 +618,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: storageURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If storage URI changes → boot diagnostics affected (In: true) - Out: false, // If VM is deleted → storage URI remains (Out: false) - }, }) // Extract DNS name from URL and create DNS link // Reference: Any attribute containing a DNS name must create a LinkedItemQuery for dns type @@ -706,10 +630,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS changes → boot diagnostics affected (In: true) - Out: false, // If VM is deleted → DNS remains (Out: false) - }, }) } } @@ -727,10 +647,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: shared.CompositeLookupKey(*vm.Name, *extension.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // If Extensions are deleted → VM remains functional (In: false) - Out: true, // If VM is deleted → Extensions become invalid/unusable (Out: true) - }, }) } } @@ -747,10 +663,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: *vm.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // If Run Commands are deleted → VM remains functional (In: false) - Out: true, // If VM is deleted → Run Commands become invalid/unusable (Out: true) - }, }) } diff --git a/sources/azure/manual/compute-virtual-machine_test.go b/sources/azure/manual/compute-virtual-machine_test.go index 857b9b64..a746365f 100644 --- a/sources/azure/manual/compute-virtual-machine_test.go +++ b/sources/azure/manual/compute-virtual-machine_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -74,67 +74,37 @@ func TestComputeVirtualMachine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "os-disk", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // dataDisks[0].managedDisk.id ExpectedType: azureshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "data-disk-1", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // networkInterfaces[0].id ExpectedType: azureshared.NetworkNetworkInterface.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nic", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // availabilitySet.id ExpectedType: azureshared.ComputeAvailabilitySet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-avset", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Resources[0] (VM Extension) - uses composite lookup key ExpectedType: azureshared.ComputeVirtualMachineExtension.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(vmName, "CustomScriptExtension"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Run commands - always linked via SEARCH ExpectedType: azureshared.ComputeVirtualMachineRunCommand.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vmName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/dbforpostgresql-database.go b/sources/azure/manual/dbforpostgresql-database.go index 0d6f3a8a..34f182b6 100644 --- a/sources/azure/manual/dbforpostgresql-database.go +++ b/sources/azure/manual/dbforpostgresql-database.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -91,10 +93,6 @@ func (s dbforPostgreSQLDatabaseWrapper) azureDBforPostgreSQLDatabaseToSDPItem(da Query: serverName, Scope: scope, // Server is in the same resource group as the database }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration, maintenance) directly affect database availability and functionality - Out: false, // Database changes (schema, data) don't directly affect the server's configuration or operation - }, // Database depends on server - server is the parent resource that hosts the database }) return sdpItem, nil @@ -140,6 +138,40 @@ func (s dbforPostgreSQLDatabaseWrapper) Search(ctx context.Context, scope string return items, nil } +func (s dbforPostgreSQLDatabaseWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(fmt.Errorf("Search requires 1 query part: serverName"), scope, s.Type())) + return + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, database := range page.Value { + if database.Name == nil { + continue + } + item, sdpErr := s.azureDBforPostgreSQLDatabaseToSDPItem(database, serverName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + // reference: GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DBforPostgreSQL/flexibleServers/{serverName}/databases/{databaseName}?api-version=2025-08-01 func (s dbforPostgreSQLDatabaseWrapper) GetLookups() sources.ItemTypeLookups { return sources.ItemTypeLookups{ diff --git a/sources/azure/manual/dbforpostgresql-database_test.go b/sources/azure/manual/dbforpostgresql-database_test.go index 43d71ff5..da9a097f 100644 --- a/sources/azure/manual/dbforpostgresql-database_test.go +++ b/sources/azure/manual/dbforpostgresql-database_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -120,12 +120,7 @@ func TestDBforPostgreSQLDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/dbforpostgresql-flexible-server.go b/sources/azure/manual/dbforpostgresql-flexible-server.go index c77f080b..b9e25f5c 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server.go @@ -6,7 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -82,6 +84,34 @@ func (s dbforPostgreSQLFlexibleServerWrapper) List(ctx context.Context, scope st return items, nil } +func (s dbforPostgreSQLFlexibleServerWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByResourceGroup(ctx, rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, server := range page.Value { + if server.Name == nil { + continue + } + item, sdpErr := s.azureDBforPostgreSQLFlexibleServerToSDPItem(server, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServerToSDPItem(server *armpostgresqlflexibleservers.Server, scope string) (*sdp.Item, *sdp.QueryError) { attributes, err := shared.ToAttributesWithExclude(server, "tags") if err != nil { @@ -133,12 +163,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: query, Scope: scope, // Use the subnet's scope, not the server's scope }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on subnet for network connectivity - // If subnet is deleted/modified, server network access may be affected - In: true, - Out: false, - }, // Subnet is an external resource that the server depends on }) // Link to Virtual Network (parent of subnet) @@ -151,12 +175,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: vnetName, Scope: scope, // Use the same scope as the subnet }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on virtual network for network connectivity - // If virtual network is deleted/modified, server network access may be affected - In: true, - Out: false, - }, // Virtual Network is an external resource that the server depends on }) } } @@ -171,10 +189,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration, maintenance) directly affect database availability - Out: false, // Database changes (schema, data) don't directly affect the server's configuration - }, // Databases are child resources that depend on their parent server }) // Link to Firewall Rules (child resource) @@ -187,10 +201,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect firewall rules - Out: true, // Firewall rule changes affect server connectivity - }, // Firewall Rules are child resources that control server access }) // Link to Configurations (child resource) @@ -203,10 +213,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect configurations - Out: true, // Configuration changes affect server behavior and performance - }, // Configurations are child resources that control server settings }) // Link to Fully Qualified Domain Name (DNS) @@ -219,10 +225,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: *server.Properties.FullyQualifiedDomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS changes affect server connectivity - Out: true, // Server changes may affect DNS resolution - }, // DNS names are shared resources that affect connectivity }) } @@ -244,12 +246,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on managed identity for authentication - // If identity is deleted/modified, server operations may fail - In: true, - Out: false, - }, }) } } @@ -274,12 +270,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: privateDNSZoneName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on private DNS zone for DNS resolution - // If DNS zone is deleted/modified, server DNS resolution may fail - In: true, - Out: false, - }, }) } } @@ -294,10 +284,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect administrators - Out: true, // Administrator changes affect server access and authentication - }, // Administrators are child resources that control server access }) // Link to Private Endpoint Connections (child resource) @@ -310,10 +296,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect private endpoint connections - Out: true, // Private endpoint connection changes affect server network connectivity - }, // Private Endpoint Connections are child resources that manage private network access }) // Link to Network Private Endpoints (external resources) from PrivateEndpointConnections @@ -337,12 +319,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: privateEndpointName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint changes (deletion, network configuration) affect the PostgreSQL Flexible Server's private connectivity - // Server deletion or configuration changes may affect the private endpoint's connection state - In: true, - Out: true, - }, // Private endpoints are tightly coupled to the server - changes affect connectivity }) } } @@ -359,10 +335,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect private link resources - Out: true, // Private link resource changes affect server private connectivity - }, // Private Link Resources are child resources that define available private link services }) // Link to Replicas (child resource) @@ -375,10 +347,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration) directly affect replica availability - Out: false, // Replica changes don't directly affect the primary server's configuration - }, // Replicas are child resources that depend on their parent server }) // Link to Migrations (child resource) @@ -391,10 +359,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect migration operations - Out: false, // Migration changes don't directly affect the server's configuration - }, // Migrations are child resources that represent data migration operations }) // Link to Backups (child resource) @@ -407,10 +371,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration) directly affect backup availability - Out: false, // Backup changes don't directly affect the server's configuration - }, // Backups are child resources that depend on their parent server }) // Link to Virtual Endpoints (child resource) @@ -423,10 +383,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration) directly affect virtual endpoint availability - Out: true, // Virtual endpoint changes affect server connectivity and routing - }, // Virtual Endpoints are child resources that control server network access }) // Link to Key Vault Vault (external resource) from Data Encryption @@ -446,12 +402,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: vaultName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on Key Vault for customer-managed encryption keys - // If Key Vault is deleted/modified, server encryption may fail - In: true, - Out: false, - }, }) } } @@ -477,12 +427,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: query, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on Key Vault key for customer-managed encryption - // If key is deleted/modified, server encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -505,12 +449,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on managed identity for accessing encryption keys - // If identity is deleted/modified, server encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -532,12 +470,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: vaultName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on Key Vault for geo-redundant backup encryption keys - // If Key Vault is deleted/modified, server geo-backup encryption may fail - In: true, - Out: false, - }, }) } } @@ -563,12 +495,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: query, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on Key Vault key for geo-redundant backup encryption - // If key is deleted/modified, server geo-backup encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -591,12 +517,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on managed identity for accessing geo-backup encryption keys - // If identity is deleted/modified, server geo-backup encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -620,12 +540,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: sourceServerName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Replica server depends on source server for replication - // If source server is deleted/modified, replica operations may fail - In: true, - Out: false, - }, }) } } diff --git a/sources/azure/manual/dbforpostgresql-flexible-server_test.go b/sources/azure/manual/dbforpostgresql-flexible-server_test.go index 600ac3b6..b6278f77 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -117,102 +117,52 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerFirewallRule.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerConfiguration.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerAdministrator.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerPrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerPrivateLinkResource.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerReplica.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerMigration.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerBackup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerVirtualEndpoint.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -249,12 +199,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != "sub-id.vnet-rg" { t.Errorf("Expected subnet link scope to be 'sub-id.vnet-rg', got %s", linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected subnet link to have In=true") - } - if linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected subnet link to have Out=false") - } } if linkedQuery.GetQuery().GetType() == azureshared.NetworkVirtualNetwork.String() { foundVNetLink = true @@ -264,12 +208,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != "sub-id.vnet-rg" { t.Errorf("Expected virtual network link scope to be 'sub-id.vnet-rg', got %s", linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected virtual network link to have In=true") - } - if linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected virtual network link to have Out=false") - } } } @@ -314,12 +252,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != "global" { t.Errorf("Expected DNS link scope to be 'global', got %s", linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected DNS link to have In=true") - } - if !linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected DNS link to have Out=true") - } } } @@ -526,9 +458,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected primary identity link to use GET method, got %v", linkedQuery.GetQuery().GetMethod()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected primary identity link to have In=true") - } } // Primary Key Vault Vault if linkedQuery.GetQuery().GetType() == azureshared.KeyVaultVault.String() && @@ -569,9 +498,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected geo backup identity link to use GET method, got %v", linkedQuery.GetQuery().GetMethod()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected geo backup identity link to have In=true") - } } } @@ -630,12 +556,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != "sub-id.source-rg" { t.Errorf("Expected source server link scope to be 'sub-id.source-rg', got %s", linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected source server link to have In=true") - } - if linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected source server link to have Out=false") - } } } diff --git a/sources/azure/manual/dns_links.go b/sources/azure/manual/dns_links.go index 8f9f96da..bda702f5 100644 --- a/sources/azure/manual/dns_links.go +++ b/sources/azure/manual/dns_links.go @@ -3,7 +3,7 @@ package manual import ( "net" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/stdlib" ) @@ -22,7 +22,6 @@ func appendDNSServerLinkIfValid(queries *[]*sdp.LinkedItemQuery, server string, Query: s, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, } }) } diff --git a/sources/azure/manual/documentdb-database-accounts.go b/sources/azure/manual/documentdb-database-accounts.go index 51afb676..28b12c20 100644 --- a/sources/azure/manual/documentdb-database-accounts.go +++ b/sources/azure/manual/documentdb-database-accounts.go @@ -6,7 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -58,6 +60,32 @@ func (s documentDBDatabaseAccountsWrapper) List(ctx context.Context, scope strin return items, nil } +func (s documentDBDatabaseAccountsWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByResourceGroup(rgScope.ResourceGroup) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, account := range page.Value { + item, sdpErr := s.azureDocumentDBDatabaseAccountToSDPItem(account, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s documentDBDatabaseAccountsWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { if len(queryParts) < 1 { return nil, &sdp.QueryError{ @@ -116,10 +144,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: *account.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint connection changes (deletion, status changes) affect the Database Account's network connectivity and accessibility - Out: true, // Database Account deletion makes the private endpoint connection invalid, and account configuration changes may affect connection status - }, // Private endpoint connections are tightly coupled to the Database Account - changes on either side affect connectivity and validity }) // Link to Private Endpoint resources @@ -149,10 +173,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: privateEndpointName, Scope: scope, // Use the private endpoint's scope, not the database account's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes (deletion, network configuration) affect the Database Account's private connectivity - Out: true, // Database Account deletion or configuration changes may affect the private endpoint's connection state - }, // Private endpoints are tightly coupled to the Database Account - changes affect connectivity }) } } @@ -193,10 +213,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: query, Scope: scope, // Use the subnet's scope, not the database account's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (deletion, network configuration) affect the Database Account's network accessibility - Out: false, // Database Account changes don't directly affect the subnet configuration - }, // Database Account depends on subnet for network access - subnet changes impact connectivity }) } } @@ -224,10 +240,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: vaultName, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Key Vault changes (key deletion, rotation, access policy) affect the Database Account's encryption - Out: false, // Database Account changes don't directly affect the Key Vault - }, // Database Account depends on Key Vault for encryption keys - key changes impact encryption/decryption }) } } @@ -271,10 +283,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: resourceGroupName, Scope: scope, // Use the identity's scope, not the database account's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Identity changes (deletion, role assignments) affect the Database Account's authentication and authorization - Out: false, // Database Account changes don't directly affect the managed identity - }, // Database Account depends on managed identity for authentication - identity changes impact access }) } } diff --git a/sources/azure/manual/documentdb-database-accounts_test.go b/sources/azure/manual/documentdb-database-accounts_test.go index e6905f10..4ebd8fb0 100644 --- a/sources/azure/manual/documentdb-database-accounts_test.go +++ b/sources/azure/manual/documentdb-database-accounts_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -112,89 +112,49 @@ func TestDocumentDBDatabaseAccounts(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - same resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - different resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint-diff-rg", ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Subnet (GET) - same resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet (GET) - different resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet-diff-rg", "test-subnet-diff-rg"), ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Key Vault (GET) ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User-Assigned Managed Identity (SEARCH) - same resource group ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: resourceGroup, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User-Assigned Managed Identity (SEARCH) - different resource group ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "identity-rg", ExpectedScope: subscriptionID + ".identity-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/keyvault-managed-hsm.go b/sources/azure/manual/keyvault-managed-hsm.go index 12be8064..5c5daad6 100644 --- a/sources/azure/manual/keyvault-managed-hsm.go +++ b/sources/azure/manual/keyvault-managed-hsm.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -129,10 +129,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: shared.CompositeLookupKey(*hsm.Name, connectionName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Connection state changes affect the Managed HSM's private connectivity - Out: true, // Managed HSM deletion removes the connection - }, }) } } @@ -167,10 +163,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: privateEndpointName, Scope: peScope, // Use the private endpoint's scope, not the Managed HSM's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes (deletion, network configuration) affect the Managed HSM's private connectivity - Out: true, // Managed HSM deletion or configuration changes may affect the private endpoint's connection state - }, }) } } @@ -211,10 +203,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: query, Scope: scope, // Use the subnet's scope, not the Managed HSM's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (deletion, network configuration) affect the Managed HSM's network accessibility - Out: false, // Managed HSM changes don't directly affect the subnet configuration - }, // Managed HSM depends on subnet for network access - subnet changes impact connectivity }) } } @@ -233,11 +221,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: *ipRule.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -265,12 +248,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Managed HSM depends on managed identity for authentication and access control - // If identity is deleted/modified, Managed HSM operations may fail - In: true, - Out: false, - }, }) } } @@ -290,11 +267,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } // Link to HTTP/HTTPS endpoint (standard library) from HsmURI @@ -305,11 +277,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: hsmURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Endpoint connectivity affects HSM access and vice versa - In: true, - Out: true, - }, }) } diff --git a/sources/azure/manual/keyvault-managed-hsm_test.go b/sources/azure/manual/keyvault-managed-hsm_test.go index 056613cc..af237d38 100644 --- a/sources/azure/manual/keyvault-managed-hsm_test.go +++ b/sources/azure/manual/keyvault-managed-hsm_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -114,133 +114,73 @@ func TestKeyVaultManagedHSM(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(hsmName, "test-pec-1"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // MHSM Private Endpoint Connection (GET) - child resource ExpectedType: azureshared.KeyVaultManagedHSMPrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(hsmName, "test-pec-2"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - same resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - different resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint-diff-rg", ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Subnet (GET) - same resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet (GET) - different resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet-diff-rg", "test-subnet-diff-rg"), ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User Assigned Managed Identity (GET) - same resource group ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User Assigned Managed Identity (GET) - different resource group ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity-diff-rg", ExpectedScope: subscriptionID + ".identity-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS (SEARCH) - from HsmURI ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: hsmName + ".managedhsm.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // HTTP (SEARCH) - from HsmURI ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://" + hsmName + ".managedhsm.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // IP (GET) - from NetworkACLs IPRules ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // IP (GET) - from NetworkACLs IPRules (CIDR range) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.0/24", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/keyvault-secret.go b/sources/azure/manual/keyvault-secret.go index b0aba42c..801e150e 100644 --- a/sources/azure/manual/keyvault-secret.go +++ b/sources/azure/manual/keyvault-secret.go @@ -5,7 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -110,6 +112,54 @@ func (k keyvaultSecretWrapper) Search(ctx context.Context, scope string, queryPa return items, nil } +func (k keyvaultSecretWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: vaultName"), scope, k.Type())) + return + } + vaultName := queryParts[0] + if vaultName == "" { + stream.SendError(azureshared.QueryError(errors.New("vaultName cannot be empty"), scope, k.Type())) + return + } + + rgScope, err := k.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, k.Type())) + return + } + pager := k.client.NewListPager(rgScope.ResourceGroup, vaultName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, k.Type())) + return + } + for _, secret := range page.Value { + if secret.Name == nil { + continue + } + var secretVaultName string + if secret.ID != nil && *secret.ID != "" { + vaultParams := azureshared.ExtractPathParamsFromResourceID(*secret.ID, []string{"vaults"}) + if len(vaultParams) > 0 { + secretVaultName = vaultParams[0] + } + } + if secretVaultName == "" { + secretVaultName = vaultName + } + item, sdpErr := k.azureSecretToSDPItem(secret, secretVaultName, *secret.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, vaultName, secretName, scope string) (*sdp.Item, *sdp.QueryError) { attributes, err := shared.ToAttributesWithExclude(secret, "tags") if err != nil { @@ -161,10 +211,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: vaultName, Scope: linkedScope, // Use the vault's scope from the resource ID }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → secret access and configuration are affected (In: true) - Out: false, // If secret is deleted → Key Vault remains (Out: false) - }, // Secret depends on Key Vault - vault changes impact secret availability and access }) } } @@ -185,10 +231,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -198,10 +240,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: secretURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -217,10 +255,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -230,10 +264,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: secretURIWithVersion, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/sources/azure/manual/keyvault-secret_test.go b/sources/azure/manual/keyvault-secret_test.go index 65a69ae4..0e8f23b1 100644 --- a/sources/azure/manual/keyvault-secret_test.go +++ b/sources/azure/manual/keyvault-secret_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -123,34 +123,19 @@ func TestKeyVaultSecret(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: vaultName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → secret access and configuration are affected - Out: false, // If secret is deleted → Key Vault remains - }, - }, - { + }, { // stdlib.NetworkDNS from SecretURI hostname ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vaultName + ".vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // stdlib.NetworkHTTP from SecretURI ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://%s.vault.azure.net/secrets/%s", vaultName, secretName), ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/keyvault-vault.go b/sources/azure/manual/keyvault-vault.go index 5116777c..de66dca1 100644 --- a/sources/azure/manual/keyvault-vault.go +++ b/sources/azure/manual/keyvault-vault.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -65,7 +65,6 @@ func (k keyvaultVaultWrapper) List(ctx context.Context, scope string) ([]*sdp.It return items, nil } - func (k keyvaultVaultWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { rgScope, err := k.ResourceGroupScopeFromScope(scope) if err != nil { @@ -136,6 +135,17 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Tags: azureshared.ConvertAzureTags(vault.Tags), } + // Child resources: list secrets in this vault (Search by vault name) + vaultName := *vault.Name + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultSecret.String(), + Method: sdp.QueryMethod_SEARCH, + Query: vaultName, + Scope: scope, + }, + }) + // Link to Private Endpoints from Private Endpoint Connections // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/private-endpoints/get // GET /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/privateEndpoints/{privateEndpointName} @@ -164,10 +174,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: privateEndpointName, Scope: scope, // Use the private endpoint's scope, not the vault's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes (deletion, network configuration) affect the Key Vault's private connectivity - Out: true, // Key Vault deletion or configuration changes may affect the private endpoint's connection state - }, // Private endpoints are tightly coupled to the Key Vault - changes affect connectivity }) } } @@ -208,10 +214,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: query, Scope: scope, // Use the subnet's scope, not the vault's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (deletion, network configuration) affect the Key Vault's network accessibility - Out: false, // Key Vault changes don't directly affect the subnet configuration - }, // Key Vault depends on subnet for network access - subnet changes impact connectivity }) } } @@ -230,11 +232,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: *ipRule.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - IP rule changes affect Key Vault network accessibility - In: true, - Out: true, - }, }) } } @@ -250,11 +247,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: vaultURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Vault endpoint connectivity affects Key Vault operations; Key Vault changes may affect endpoint - In: true, - Out: true, - }, }) } @@ -284,10 +276,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: hsmName, Scope: scope, // Use the Managed HSM's scope, not the vault's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Managed HSM changes (deletion, configuration) affect the Key Vault's functionality and availability - Out: false, // Key Vault changes don't directly affect the Managed HSM itself - }, // Key Vault depends on Managed HSM for hardware-backed security - HSM changes impact vault operations }) } } @@ -313,6 +301,7 @@ func (k keyvaultVaultWrapper) TerraformMappings() []*sdp.TerraformMapping { func (k keyvaultVaultWrapper) PotentialLinks() map[shared.ItemType]bool { return shared.NewItemTypesSet( + azureshared.KeyVaultSecret, azureshared.NetworkPrivateEndpoint, azureshared.NetworkSubnet, azureshared.KeyVaultManagedHSM, diff --git a/sources/azure/manual/keyvault-vault_test.go b/sources/azure/manual/keyvault-vault_test.go index ce440e9c..267d6955 100644 --- a/sources/azure/manual/keyvault-vault_test.go +++ b/sources/azure/manual/keyvault-vault_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -108,94 +108,60 @@ func TestKeyVaultVault(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ { + // Child resources: secrets in this vault (SEARCH by vault name) + ExpectedType: azureshared.KeyVaultSecret.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: vaultName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, { // Private Endpoint (GET) - same resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - different resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint-diff-rg", ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Subnet (GET) - same resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet (GET) - different resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet-diff-rg", "test-subnet-diff-rg"), ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Managed HSM (GET) - different resource group ExpectedType: azureshared.KeyVaultManagedHSM.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-managed-hsm", ExpectedScope: subscriptionID + ".hsm-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // stdlib.NetworkIP (GET) - from NetworkACLs IPRules ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.100", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // stdlib.NetworkIP (GET) - from NetworkACLs IPRules (CIDR range) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.0/24", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // stdlib.NetworkHTTP (SEARCH) - from VaultURI ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://test-keyvault.vault.azure.net/", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -254,9 +220,9 @@ func TestKeyVaultVault(t *testing.T) { t.Fatalf("Expected no error, got: %v", qErr) } - // Should have no linked item queries - if len(sdpItem.GetLinkedItemQueries()) != 0 { - t.Errorf("Expected no linked item queries, got %d", len(sdpItem.GetLinkedItemQueries())) + // Should only have the child SEARCH link (secrets in vault); no private endpoints, subnets, etc. + if len(sdpItem.GetLinkedItemQueries()) != 1 { + t.Errorf("Expected 1 linked item query (KeyVaultSecret SEARCH), got %d", len(sdpItem.GetLinkedItemQueries())) } }) diff --git a/sources/azure/manual/links_helpers.go b/sources/azure/manual/links_helpers.go index 40b84657..46302efc 100644 --- a/sources/azure/manual/links_helpers.go +++ b/sources/azure/manual/links_helpers.go @@ -1,7 +1,11 @@ package manual import ( - "github.com/overmindtech/cli/sdp-go" + "net" + "strings" + + "github.com/overmindtech/cli/go/sdp-go" + azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -27,6 +31,61 @@ func appendLinkIfValid( } } +// AppendURILinks appends linked item queries for a URI: HTTP link plus DNS or IP link from the host (with deduplication). +// It mutates linkedItemQueries and the dedupe maps. Skips empty or non-http(s) URIs. +// blastIn and blastOut set BlastPropagation for the added HTTP/DNS/IP links. +func AppendURILinks( + linkedItemQueries *[]*sdp.LinkedItemQuery, + uri string, + linkedDNSHostnames map[string]struct{}, + seenIPs map[string]struct{}, + blastIn, blastOut bool, +) { + if uri == "" || (!strings.HasPrefix(uri, "http://") && !strings.HasPrefix(uri, "https://")) { + return + } + *linkedItemQueries = append(*linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: uri, + Scope: "global", + }, + }) + hostFromURL := azureshared.ExtractDNSFromURL(uri) + if hostFromURL != "" { + hostOnly := hostFromURL + if h, _, err := net.SplitHostPort(hostFromURL); err == nil { + hostOnly = h + } + if net.ParseIP(hostOnly) != nil { + if _, seen := seenIPs[hostOnly]; !seen { + seenIPs[hostOnly] = struct{}{} + *linkedItemQueries = append(*linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: hostOnly, + Scope: "global", + }, + }) + } + } else { + if _, seen := linkedDNSHostnames[hostOnly]; !seen { + linkedDNSHostnames[hostOnly] = struct{}{} + *linkedItemQueries = append(*linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: hostOnly, + Scope: "global", + }, + }) + } + } + } +} + // networkIPQuery returns a linked item query for stdlib.NetworkIP. func networkIPQuery(query string) *sdp.LinkedItemQuery { return &sdp.LinkedItemQuery{ @@ -36,6 +95,5 @@ func networkIPQuery(query string) *sdp.LinkedItemQuery { Query: query, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, } } diff --git a/sources/azure/manual/managedidentity-user-assigned-identity.go b/sources/azure/manual/managedidentity-user-assigned-identity.go index c8e3258f..1873629b 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -117,12 +117,6 @@ func (m managedIdentityUserAssignedIdentityWrapper) azureManagedIdentityUserAssi Query: *identity.Name, // Identity name is sufficient since resource group is available to the adapter Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Federated credentials are tightly coupled to the identity - // Changes to the identity affect credentials, and credential changes affect identity usage - In: true, - Out: true, - }, }) return sdpItem, nil diff --git a/sources/azure/manual/managedidentity-user-assigned-identity_test.go b/sources/azure/manual/managedidentity-user-assigned-identity_test.go index 67716545..219dd86f 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -71,12 +71,7 @@ func TestManagedIdentityUserAssignedIdentity(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: identityName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/mock_gallery_application_versions_client_test.go b/sources/azure/manual/mock_gallery_application_versions_client_test.go new file mode 100644 index 00000000..e86f52f3 --- /dev/null +++ b/sources/azure/manual/mock_gallery_application_versions_client_test.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sources/azure/clients/gallery-application-versions-client.go +// +// Generated by this command: +// +// mockgen -destination=sources/azure/manual/mock_gallery_application_versions_client_test.go -package=manual -source=sources/azure/clients/gallery-application-versions-client.go +// + +// Package manual is a generated GoMock package. +package manual + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleryApplicationVersionsClient is a mock of GalleryApplicationVersionsClient interface. +type MockGalleryApplicationVersionsClient struct { + ctrl *gomock.Controller + recorder *MockGalleryApplicationVersionsClientMockRecorder + isgomock struct{} +} + +// MockGalleryApplicationVersionsClientMockRecorder is the mock recorder for MockGalleryApplicationVersionsClient. +type MockGalleryApplicationVersionsClientMockRecorder struct { + mock *MockGalleryApplicationVersionsClient +} + +// NewMockGalleryApplicationVersionsClient creates a new mock instance. +func NewMockGalleryApplicationVersionsClient(ctrl *gomock.Controller) *MockGalleryApplicationVersionsClient { + mock := &MockGalleryApplicationVersionsClient{ctrl: ctrl} + mock.recorder = &MockGalleryApplicationVersionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryApplicationVersionsClient) EXPECT() *MockGalleryApplicationVersionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryApplicationVersionsClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) + ret0, _ := ret[0].(armcompute.GalleryApplicationVersionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryApplicationVersionsClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) +} + +// NewListByGalleryApplicationPager mocks base method. +func (m *MockGalleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) clients.GalleryApplicationVersionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryApplicationPager", resourceGroupName, galleryName, galleryApplicationName, options) + ret0, _ := ret[0].(clients.GalleryApplicationVersionsPager) + return ret0 +} + +// NewListByGalleryApplicationPager indicates an expected call of NewListByGalleryApplicationPager. +func (mr *MockGalleryApplicationVersionsClientMockRecorder) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryApplicationPager", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).NewListByGalleryApplicationPager), resourceGroupName, galleryName, galleryApplicationName, options) +} diff --git a/sources/azure/manual/mock_gallery_images_client_test.go b/sources/azure/manual/mock_gallery_images_client_test.go new file mode 100644 index 00000000..58709319 --- /dev/null +++ b/sources/azure/manual/mock_gallery_images_client_test.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sources/azure/clients/gallery-images-client.go +// +// Generated by this command: +// +// mockgen -destination=sources/azure/manual/mock_gallery_images_client_test.go -package=manual -source=sources/azure/clients/gallery-images-client.go +// + +// Package manual is a generated GoMock package. +package manual + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleryImagesClient is a mock of GalleryImagesClient interface. +type MockGalleryImagesClient struct { + ctrl *gomock.Controller + recorder *MockGalleryImagesClientMockRecorder + isgomock struct{} +} + +// MockGalleryImagesClientMockRecorder is the mock recorder for MockGalleryImagesClient. +type MockGalleryImagesClientMockRecorder struct { + mock *MockGalleryImagesClient +} + +// NewMockGalleryImagesClient creates a new mock instance. +func NewMockGalleryImagesClient(ctrl *gomock.Controller) *MockGalleryImagesClient { + mock := &MockGalleryImagesClient{ctrl: ctrl} + mock.recorder = &MockGalleryImagesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryImagesClient) EXPECT() *MockGalleryImagesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryImagesClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryImageName string, options *armcompute.GalleryImagesClientGetOptions) (armcompute.GalleryImagesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryImageName, options) + ret0, _ := ret[0].(armcompute.GalleryImagesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryImagesClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryImageName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryImagesClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryImageName, options) +} + +// NewListByGalleryPager mocks base method. +func (m *MockGalleryImagesClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) clients.GalleryImagesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryPager", resourceGroupName, galleryName, options) + ret0, _ := ret[0].(clients.GalleryImagesPager) + return ret0 +} + +// NewListByGalleryPager indicates an expected call of NewListByGalleryPager. +func (mr *MockGalleryImagesClientMockRecorder) NewListByGalleryPager(resourceGroupName, galleryName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryPager", reflect.TypeOf((*MockGalleryImagesClient)(nil).NewListByGalleryPager), resourceGroupName, galleryName, options) +} diff --git a/sources/azure/manual/network-application-gateway.go b/sources/azure/manual/network-application-gateway.go index 76288cd9..a4511f87 100644 --- a/sources/azure/manual/network-application-gateway.go +++ b/sources/azure/manual/network-application-gateway.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -125,10 +125,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *gatewayIPConfig.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // GatewayIPConfiguration changes affect the Application Gateway's network configuration - Out: true, // Application Gateway changes (like deletion) affect the gateway IP configuration - }, }) // Link to Subnet from GatewayIPConfiguration @@ -150,10 +146,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes affect the Application Gateway's network configuration - Out: false, // Application Gateway changes don't affect the subnet itself - }, }) // Link to VirtualNetwork (extracted from subnet ID) @@ -168,10 +160,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: vnetName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // VirtualNetwork changes affect the Application Gateway's network configuration - Out: false, // Application Gateway changes don't affect the virtual network itself - }, }) } } @@ -191,10 +179,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *frontendIPConfig.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // FrontendIPConfiguration changes affect the Application Gateway's frontend configuration - Out: true, // Application Gateway changes (like deletion) affect the frontend IP configuration - }, }) } @@ -215,10 +199,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: publicIPName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP changes affect the Application Gateway's frontend configuration - Out: false, // Application Gateway changes don't affect the public IP address itself - }, }) } } @@ -242,10 +222,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes affect the Application Gateway's frontend configuration - Out: false, // Application Gateway changes don't affect the subnet itself - }, }) } } @@ -259,10 +235,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *frontendIPConfig.Properties.PrivateIPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // IPs are always linked bidirectionally - Out: true, - }, }) } } @@ -281,10 +253,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *backendPool.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // BackendAddressPool changes affect which backends receive traffic - Out: true, // Application Gateway changes (like deletion) affect the backend address pool - }, }) } @@ -299,10 +267,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *backendAddress.IPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // IPs are always linked bidirectionally - Out: true, - }, }) } @@ -315,10 +279,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *backendAddress.Fqdn, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS names are always linked bidirectionally - Out: true, - }, }) } } @@ -338,10 +298,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *httpListener.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // HTTPListener changes affect how the Application Gateway receives traffic - Out: true, // Application Gateway changes (like deletion) affect the HTTP listener - }, }) } @@ -357,10 +313,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *httpListener.Properties.HostName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect how the Application Gateway receives traffic - Out: true, // DNS names are always linked bidirectionally - }, }) } @@ -375,10 +327,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *hostName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect how the Application Gateway receives traffic - Out: true, // DNS names are always linked bidirectionally - }, }) } } @@ -399,10 +347,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *backendHTTPSettings.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // BackendHTTPSettings changes affect how the Application Gateway communicates with backends - Out: true, // Application Gateway changes (like deletion) affect the backend HTTP settings - }, }) } @@ -416,10 +360,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *backendHTTPSettings.Properties.HostName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect backend communication - Out: true, // DNS names are always linked bidirectionally - }, }) } } @@ -437,10 +377,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *rule.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // RequestRoutingRule changes affect how traffic is routed - Out: true, // Application Gateway changes (like deletion) affect the routing rule - }, }) } } @@ -458,10 +394,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *probe.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Probe changes affect backend health monitoring - Out: true, // Application Gateway changes (like deletion) affect the probe - }, }) } @@ -475,10 +407,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *probe.Properties.Host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect health probe targets - Out: true, // DNS names are always linked bidirectionally - }, }) } } @@ -496,10 +424,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *sslCert.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SSLCertificate changes affect HTTPS listeners - Out: true, // Application Gateway changes (like deletion) affect the SSL certificate - }, }) } @@ -518,10 +442,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(vaultName, secretName), Scope: n.DefaultScope(), // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Secret is deleted/modified → SSL certificate access is affected (In: true) - Out: false, // If Application Gateway is deleted → Key Vault Secret remains (Out: false) - }, }) } @@ -535,11 +455,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked bidirectionally - In: true, - Out: true, - }, }) } } @@ -558,10 +473,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *urlPathMap.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // URLPathMap changes affect path-based routing - Out: true, // Application Gateway changes (like deletion) affect the URL path map - }, }) } } @@ -579,10 +490,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *authCert.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // AuthenticationCertificate changes affect backend authentication - Out: true, // Application Gateway changes (like deletion) affect the authentication certificate - }, }) } } @@ -600,10 +507,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *trustedRootCert.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // TrustedRootCertificate changes affect backend server validation - Out: true, // Application Gateway changes (like deletion) affect the trusted root certificate - }, }) } @@ -622,10 +525,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(vaultName, secretName), Scope: n.DefaultScope(), // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Secret is deleted/modified → TrustedRootCertificate access is affected (In: true) - Out: false, // If Application Gateway is deleted → Key Vault Secret remains (Out: false) - }, }) } @@ -639,11 +538,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked bidirectionally - In: true, - Out: true, - }, }) } } @@ -662,10 +556,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *rewriteRuleSet.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // RewriteRuleSet changes affect request/response modification - Out: true, // Application Gateway changes (like deletion) affect the rewrite rule set - }, }) } } @@ -683,10 +573,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *redirectConfig.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // RedirectConfiguration changes affect URL redirection behavior - Out: true, // Application Gateway changes (like deletion) affect the redirect configuration - }, }) } @@ -702,10 +588,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect redirect target availability - Out: true, // DNS names are always linked bidirectionally - }, }) } } @@ -728,10 +610,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: firewallPolicyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // WAF Policy changes affect the Application Gateway's security configuration - Out: false, // Application Gateway changes don't affect the WAF policy itself - }, }) } } @@ -754,12 +632,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: identityName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Application Gateway depends on managed identity for authentication (e.g., Key Vault integration for SSL certificates) - // If identity is deleted/modified, Application Gateway operations may fail - In: true, - Out: false, - }, }) } } diff --git a/sources/azure/manual/network-application-gateway_test.go b/sources/azure/manual/network-application-gateway_test.go index 53afb962..17bd4d56 100644 --- a/sources/azure/manual/network-application-gateway_test.go +++ b/sources/azure/manual/network-application-gateway_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -72,265 +72,145 @@ func TestNetworkApplicationGateway(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "gateway-ip-config"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Subnet from GatewayIPConfiguration ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // VirtualNetwork from GatewayIPConfiguration subnet ExpectedType: azureshared.NetworkVirtualNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vnet", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // FrontendIPConfiguration child resource ExpectedType: azureshared.NetworkApplicationGatewayFrontendIPConfiguration.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "frontend-ip-config"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // PublicIPAddress external resource ExpectedType: azureshared.NetworkPublicIPAddress.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-public-ip", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Private IP address link (standard library) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.2.0.5", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // BackendAddressPool child resource ExpectedType: azureshared.NetworkApplicationGatewayBackendAddressPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "backend-pool"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Backend IP address link (standard library) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.1.4", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // HTTPListener child resource ExpectedType: azureshared.NetworkApplicationGatewayHTTPListener.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "http-listener"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // BackendHTTPSettings child resource ExpectedType: azureshared.NetworkApplicationGatewayBackendHTTPSettings.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "backend-http-settings"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // RequestRoutingRule child resource ExpectedType: azureshared.NetworkApplicationGatewayRequestRoutingRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "routing-rule"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Probe child resource ExpectedType: azureshared.NetworkApplicationGatewayProbe.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "health-probe"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // SSLCertificate child resource ExpectedType: azureshared.NetworkApplicationGatewaySSLCertificate.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "ssl-cert"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Key Vault Secret from SSLCertificate KeyVaultSecretID ExpectedType: azureshared.KeyVaultSecret.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-keyvault", "test-secret"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS name from SSLCertificate KeyVaultSecretID ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-keyvault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // URLPathMap child resource ExpectedType: azureshared.NetworkApplicationGatewayURLPathMap.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "url-path-map"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // AuthenticationCertificate child resource ExpectedType: azureshared.NetworkApplicationGatewayAuthenticationCertificate.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "auth-cert"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // TrustedRootCertificate child resource ExpectedType: azureshared.NetworkApplicationGatewayTrustedRootCertificate.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "trusted-root-cert"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Key Vault Secret from TrustedRootCertificate KeyVaultSecretID ExpectedType: azureshared.KeyVaultSecret.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-trusted-keyvault", "test-trusted-secret"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS name from TrustedRootCertificate KeyVaultSecretID ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-trusted-keyvault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // RewriteRuleSet child resource ExpectedType: azureshared.NetworkApplicationGatewayRewriteRuleSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "rewrite-rule-set"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // RedirectConfiguration child resource ExpectedType: azureshared.NetworkApplicationGatewayRedirectConfiguration.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "redirect-config"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // WAF Policy external resource ExpectedType: azureshared.NetworkApplicationGatewayWebApplicationFirewallPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-waf-policy", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User Assigned Managed Identity external resource ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-load-balancer.go b/sources/azure/manual/network-load-balancer.go index 187b50fe..4369d479 100644 --- a/sources/azure/manual/network-load-balancer.go +++ b/sources/azure/manual/network-load-balancer.go @@ -7,14 +7,14 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" ) var NetworkLoadBalancerLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkLoadBalancer) @@ -66,7 +66,6 @@ func (n networkLoadBalancerWrapper) List(ctx context.Context, scope string) ([]* return items, nil } - func (n networkLoadBalancerWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { rgScope, err := n.ResourceGroupScopeFromScope(scope) if err != nil { @@ -155,10 +154,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *frontendIPConfig.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // FrontendIPConfiguration changes affect the load balancer's frontend configuration - Out: true, // Load balancer changes (like deletion) affect the frontend IP configuration - }, // FrontendIPConfiguration is a child resource of the Load Balancer; bidirectional dependency }) } @@ -185,10 +180,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: publicIPName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP changes (like deletion or reassignment) affect the load balancer's frontend - Out: false, // Load balancer changes don't affect the public IP address itself - }, // Public IP provides the frontend IP for the load balancer }) } } @@ -211,10 +202,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (like address space modifications) affect the load balancer's network configuration - Out: false, // Load balancer changes don't affect the subnet itself - }, }) } } @@ -235,10 +222,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(params[0], params[1]), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Gateway LB frontend changes affect this load balancer's chained configuration - Out: false, // This LB changes don't affect the gateway LB frontend - }, }) } } @@ -259,10 +242,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: publicIPPrefixName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP prefix changes affect the load balancer's frontend allocation - Out: false, // Load balancer changes don't affect the public IP prefix - }, }) } } @@ -276,11 +255,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: *frontendIPConfig.Properties.PrivateIPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -300,10 +274,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *backendPool.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // BackendAddressPool changes affect which backends receive traffic - Out: true, // Load balancer changes (like deletion) affect the backend address pool - }, }) } @@ -323,10 +293,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: vnetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // VNet changes affect backend pool scope/connectivity - Out: false, // Load balancer changes don't affect the virtual network - }, }) } } @@ -352,10 +318,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(params[0], params[1]), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -374,10 +336,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(params[0], params[1]), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -396,10 +354,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: vnetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -412,10 +366,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: *addr.Properties.IPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -436,10 +386,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *natRule.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // InboundNatRule changes affect the load balancer's NAT configuration - Out: true, // Load balancer changes (like deletion) affect the NAT rules - }, // InboundNatRule is a child resource of the Load Balancer; bidirectional dependency }) } @@ -462,10 +408,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: nicName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Network interface changes affect NAT rule routing - Out: true, // NAT rule changes affect which network interface receives inbound traffic - }, // Inbound NAT rules map traffic to specific network interfaces; bidirectional operational dependency }) } } @@ -485,10 +427,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *lbRule.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // LoadBalancingRule changes affect how traffic is distributed - Out: true, // Load balancer changes (like deletion) affect the load balancing rules - }, // LoadBalancingRule is a child resource of the Load Balancer; bidirectional dependency }) } } @@ -507,10 +445,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *probe.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Probe changes affect health monitoring of backend instances - Out: true, // Load balancer changes (like deletion) affect the probes - }, // Probe is a child resource of the Load Balancer; bidirectional dependency }) } } @@ -529,10 +463,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *outboundRule.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // OutboundRule changes affect outbound connectivity configuration - Out: true, // Load balancer changes (like deletion) affect the outbound rules - }, // OutboundRule is a child resource of the Load Balancer; bidirectional dependency }) } } @@ -551,10 +481,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *natPool.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // InboundNatPool changes affect NAT pool configuration - Out: true, // Load balancer changes (like deletion) affect the NAT pools - }, // InboundNatPool is a child resource of the Load Balancer; bidirectional dependency }) } } diff --git a/sources/azure/manual/network-load-balancer_test.go b/sources/azure/manual/network-load-balancer_test.go index 92d6b5cc..e20ead69 100644 --- a/sources/azure/manual/network-load-balancer_test.go +++ b/sources/azure/manual/network-load-balancer_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -71,122 +71,67 @@ func TestNetworkLoadBalancer(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "frontend-ip-config"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // PublicIPAddress external resource ExpectedType: azureshared.NetworkPublicIPAddress.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-public-ip", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet external resource ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Private IP address link (standard library) ExpectedType: "ip", ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.2.0.5", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // BackendAddressPool child resource ExpectedType: azureshared.NetworkLoadBalancerBackendAddressPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "backend-pool"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // InboundNatRule child resource ExpectedType: azureshared.NetworkLoadBalancerInboundNatRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "inbound-nat-rule"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // NetworkInterface via InboundNatRule BackendIPConfiguration ExpectedType: azureshared.NetworkNetworkInterface.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nic", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // LoadBalancingRule child resource ExpectedType: azureshared.NetworkLoadBalancerLoadBalancingRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "lb-rule"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Probe child resource ExpectedType: azureshared.NetworkLoadBalancerProbe.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "probe"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // OutboundRule child resource ExpectedType: azureshared.NetworkLoadBalancerOutboundRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "outbound-rule"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // InboundNatPool child resource ExpectedType: azureshared.NetworkLoadBalancerInboundNatPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "nat-pool"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-network-interface.go b/sources/azure/manual/network-network-interface.go index 1e8fe0f9..7c7d75e9 100644 --- a/sources/azure/manual/network-network-interface.go +++ b/sources/azure/manual/network-network-interface.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" ) var NetworkNetworkInterfaceLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkNetworkInterface) @@ -60,7 +60,6 @@ func (n networkNetworkInterfaceWrapper) List(ctx context.Context, scope string) return items, nil } - func (n networkNetworkInterfaceWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { rgScope, err := n.ResourceGroupScopeFromScope(scope) if err != nil { @@ -88,6 +87,7 @@ func (n networkNetworkInterfaceWrapper) ListStream(ctx context.Context, stream d } } } + // reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/network-interfaces/get?view=rest-virtualnetwork-2025-03-01&tabs=HTTP#response func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkInterface *armnetwork.Interface) (*sdp.Item, *sdp.QueryError) { if networkInterface.Name == nil { @@ -115,10 +115,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: *networkInterface.Name, Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // IP configuration changes don't affect the network interface itself - Out: true, // Network interface changes (especially deletion) affect IP configurations - }, // IP configurations are child resources of the network interface }) if networkInterface.Properties != nil && networkInterface.Properties.VirtualMachine != nil { @@ -132,10 +128,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: vmName, Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // VM changes (like deletion) may detach the interface but don't delete it - Out: true, // Network interface changes/deletion directly affect VM network connectivity - }, // Network interface provides connectivity to the VM; bidirectional operational dependency }) } } @@ -152,10 +144,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: nsgName, Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // NSG rule changes affect the network interface's security and traffic flow - Out: false, // Network interface changes don't affect the NSG itself - }, // NSG controls security rules applied to the network interface }) } } @@ -177,10 +165,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: peName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes affect the NIC's role - Out: true, // NIC changes affect the private endpoint's connectivity - }, }) } } @@ -201,10 +185,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: plsName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private link service changes affect the NIC - Out: true, // NIC changes affect the private link service - }, }) } } @@ -225,10 +205,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: dscpName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DSCP config changes affect NIC QoS - Out: false, // NIC changes don't affect the DSCP configuration resource - }, }) } } @@ -241,10 +217,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: *networkInterface.Name, Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Tap config changes don't affect the NIC itself - Out: true, // NIC changes (e.g. deletion) affect tap configurations - }, }) // IP configuration references: subnet, public IP, private IP (stdlib), ASGs, LB pools/rules, App Gateway pools, gateway LB, VNet taps @@ -271,10 +243,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (e.g. address space) affect the NIC's IP config - Out: false, // NIC changes don't affect the subnet resource - }, }) } } @@ -294,10 +262,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: pipName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP changes affect the NIC's connectivity - Out: true, // NIC detachment affects the public IP's association - }, }) } } @@ -312,10 +276,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: addr, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -336,10 +296,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG rule changes affect the NIC's effective rules - Out: false, // NIC changes don't affect the ASG - }, }) } } @@ -363,10 +319,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(params[0], params[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Pool config changes affect which backends receive traffic - Out: true, // NIC removal affects the pool's members - }, }) } } @@ -390,10 +342,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(params[0], params[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // NAT rule changes affect the NIC - Out: true, // NIC removal affects the NAT rule's target - }, }) } } @@ -417,10 +365,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(params[0], params[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // App GW pool changes affect backend targets - Out: true, // NIC removal affects the pool's members - }, }) } } @@ -442,10 +386,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(params[0], params[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Gateway LB frontend changes affect traffic path - Out: true, // NIC changes affect the gateway LB association - }, }) } } @@ -467,10 +407,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: tapName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Tap config changes affect what is mirrored - Out: true, // NIC removal affects the tap's sources - }, }) } } @@ -498,10 +434,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: *dns.InternalDNSNameLabel, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if dns.InternalFqdn != nil && *dns.InternalFqdn != "" { @@ -512,10 +444,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: *dns.InternalFqdn, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/network-network-interface_test.go b/sources/azure/manual/network-network-interface_test.go index d491ac3e..a7f632c5 100644 --- a/sources/azure/manual/network-network-interface_test.go +++ b/sources/azure/manual/network-network-interface_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -71,45 +71,25 @@ func TestNetworkNetworkInterface(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nicName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // ComputeVirtualMachine link ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // NetworkNetworkSecurityGroup link ExpectedType: azureshared.NetworkNetworkSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nsg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // NetworkNetworkInterfaceTapConfiguration link (child resource) ExpectedType: azureshared.NetworkNetworkInterfaceTapConfiguration.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nicName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -137,62 +117,32 @@ func TestNetworkNetworkInterface(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nicName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.NetworkNetworkSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nsg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.NetworkNetworkInterfaceTapConfiguration.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nicName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "dns.internal", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) }) diff --git a/sources/azure/manual/network-network-security-group.go b/sources/azure/manual/network-network-security-group.go index 30e19904..5e072ab1 100644 --- a/sources/azure/manual/network-network-security-group.go +++ b/sources/azure/manual/network-network-security-group.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -152,10 +152,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: shared.CompositeLookupKey(nsgName, *securityRule.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Security rule changes affect the NSG's behavior - Out: false, // NSG changes don't affect individual rules (rules are part of NSG) - }, }) } } @@ -173,10 +169,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: shared.CompositeLookupKey(nsgName, *defaultSecurityRule.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Default security rule changes affect the NSG's behavior - Out: false, // NSG changes don't affect individual default rules (rules are part of NSG) - }, }) } } @@ -213,10 +205,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (like deletion) affect the NSG association - Out: true, // NSG rule changes affect traffic in the subnet - }, }) } } @@ -243,10 +231,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: nicName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Network interface changes don't affect the NSG - Out: true, // NSG rule changes affect traffic on the network interface - }, }) } } @@ -277,10 +261,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: shared.CompositeLookupKey(networkWatcherName, flowLogName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Flow log config changes affect the NSG's observability - Out: false, // NSG changes don't affect the flow log resource - }, }) } } @@ -313,10 +293,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG changes affect the security rule's source criteria - Out: false, // Security rule changes don't affect the ASG - }, }) } } @@ -341,10 +317,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG changes affect the security rule's destination criteria - Out: false, // Security rule changes don't affect the ASG - }, }) } } @@ -394,10 +366,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG changes affect the default security rule's source criteria - Out: false, // Default security rule changes don't affect the ASG - }, }) } } @@ -422,10 +390,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG changes affect the default security rule's destination criteria - Out: false, // Default security rule changes don't affect the ASG - }, }) } } diff --git a/sources/azure/manual/network-network-security-group_test.go b/sources/azure/manual/network-network-security-group_test.go index 559c196a..e79071fd 100644 --- a/sources/azure/manual/network-network-security-group_test.go +++ b/sources/azure/manual/network-network-security-group_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -71,78 +71,43 @@ func TestNetworkNetworkSecurityGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(nsgName, "test-security-rule"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DefaultSecurityRule link ExpectedType: azureshared.NetworkDefaultSecurityRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(nsgName, "AllowVnetInBound"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet link ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // NetworkInterface link ExpectedType: azureshared.NetworkNetworkInterface.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nic", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // ApplicationSecurityGroup link (from SecurityRule Source) ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-asg-source", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // ApplicationSecurityGroup link (from SecurityRule Destination) ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-asg-dest", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // ApplicationSecurityGroup link (from DefaultSecurityRule Source) ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-asg-default-source", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-public-ip-address.go b/sources/azure/manual/network-public-ip-address.go index f06ed5ff..eecf5e3e 100644 --- a/sources/azure/manual/network-public-ip-address.go +++ b/sources/azure/manual/network-public-ip-address.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -94,6 +94,7 @@ func (n networkPublicIPAddressWrapper) ListStream(ctx context.Context, stream di } } } + // reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/public-ip-addresses/get?view=rest-virtualnetwork-2025-03-01&tabs=HTTP // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/publicIPAddresses/{publicIpAddressName}?api-version=2025-03-01 func (n networkPublicIPAddressWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { @@ -142,11 +143,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: *publicIPAddress.Properties.IPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -159,11 +155,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: *publicIPAddress.Properties.DNSSettings.Fqdn, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } @@ -188,10 +179,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: nicName[0], Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Network interface IP configuration changes affect the public IP address - Out: false, // Public IP address changes don't affect the network interface itself - }, // Public IP address is associated with the network interface's IP configuration }) } } @@ -216,10 +203,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: linkedIPName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Linked public IP address changes can affect this public IP address - Out: true, // This public IP address changes can affect the linked public IP address - }, // Linked public IP addresses are tightly coupled and affect each other }) } } @@ -243,10 +226,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: serviceIPName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Service public IP address changes can affect this public IP address - Out: false, // This public IP address changes don't affect the service public IP address - }, // Service public IP address is the underlying resource for this public IP address }) } } @@ -270,10 +249,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: prefixName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP prefix changes can affect this public IP address - Out: false, // This public IP address changes don't affect the public IP prefix - }, // Public IP address is allocated from the public IP prefix }) } } @@ -297,10 +272,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: natGatewayName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // NAT gateway changes can affect this public IP address - Out: false, // This public IP address changes don't affect the NAT gateway - }, // Public IP address is associated with the NAT gateway for outbound connectivity }) } } @@ -325,10 +296,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: ddosPlanName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DDoS protection plan changes can affect this public IP address protection - Out: false, // This public IP address changes don't affect the DDoS protection plan - }, // Public IP address is protected by the DDoS protection plan }) } } @@ -356,10 +323,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: lbName[0], Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Load balancer frontend IP configuration changes affect the public IP address - Out: false, // Public IP address changes don't affect the load balancer itself - }, // Public IP address is associated with the load balancer's frontend IP configuration }) } } diff --git a/sources/azure/manual/network-public-ip-address_test.go b/sources/azure/manual/network-public-ip-address_test.go index 6da3eeff..f8971218 100644 --- a/sources/azure/manual/network-public-ip-address_test.go +++ b/sources/azure/manual/network-public-ip-address_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -71,56 +71,31 @@ func TestNetworkPublicIPAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // NetworkNetworkInterface link (via IPConfiguration) ExpectedType: azureshared.NetworkNetworkInterface.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nic", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // NetworkPublicIPPrefix link ExpectedType: azureshared.NetworkPublicIPPrefix.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-prefix", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // NetworkNatGateway link ExpectedType: azureshared.NetworkNatGateway.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nat-gateway", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // NetworkDdosProtectionPlan link ExpectedType: azureshared.NetworkDdosProtectionPlan.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-ddos-plan", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -184,9 +159,6 @@ func TestNetworkPublicIPAddress(t *testing.T) { if linkedQuery.GetQuery().GetType() == azureshared.NetworkPublicIPAddress.String() && linkedQuery.GetQuery().GetQuery() == linkedIPName { foundLinkedIP = true - if linkedQuery.GetBlastPropagation().GetIn() != true || linkedQuery.GetBlastPropagation().GetOut() != true { - t.Error("Expected linked public IP to have In: true, Out: true") - } break } } @@ -220,9 +192,6 @@ func TestNetworkPublicIPAddress(t *testing.T) { if linkedQuery.GetQuery().GetType() == azureshared.NetworkPublicIPAddress.String() && linkedQuery.GetQuery().GetQuery() == serviceIPName { foundServiceIP = true - if linkedQuery.GetBlastPropagation().GetIn() != true || linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected service public IP to have In: true, Out: false") - } break } } diff --git a/sources/azure/manual/network-route-table.go b/sources/azure/manual/network-route-table.go index 0194ef5a..e0c1e839 100644 --- a/sources/azure/manual/network-route-table.go +++ b/sources/azure/manual/network-route-table.go @@ -5,16 +5,15 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) - var NetworkRouteTableLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkRouteTable) type networkRouteTableWrapper struct { @@ -122,10 +121,6 @@ func (n networkRouteTableWrapper) azureRouteTableToSDPItem(routeTable *armnetwor Query: shared.CompositeLookupKey(routeTableName, *route.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Route changes affect the route table's routing behavior - Out: false, // Route table changes don't affect individual routes (routes are part of the table) - }, }) // Link to NextHopIPAddress (IP address to stdlib) @@ -138,11 +133,6 @@ func (n networkRouteTableWrapper) azureRouteTableToSDPItem(routeTable *armnetwor Query: *route.Properties.NextHopIPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked bidirectionally - In: true, - Out: true, - }, }) } @@ -180,10 +170,6 @@ func (n networkRouteTableWrapper) azureRouteTableToSDPItem(routeTable *armnetwor Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (like route table association) affect the route table's usage - Out: true, // Route table changes affect traffic routing in the subnet - }, }) } } diff --git a/sources/azure/manual/network-route-table_test.go b/sources/azure/manual/network-route-table_test.go index 3dd23909..56dbdb73 100644 --- a/sources/azure/manual/network-route-table_test.go +++ b/sources/azure/manual/network-route-table_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -71,10 +71,6 @@ func TestNetworkRouteTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(routeTableName, "test-route"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Route with NextHopIPAddress link (IP address to stdlib) @@ -82,10 +78,6 @@ func TestNetworkRouteTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // Subnet link (external resource) @@ -93,10 +85,6 @@ func TestNetworkRouteTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/azure/manual/network-virtual-network.go b/sources/azure/manual/network-virtual-network.go index 4cc91fac..1d24fe8b 100644 --- a/sources/azure/manual/network-virtual-network.go +++ b/sources/azure/manual/network-virtual-network.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" ) var NetworkVirtualNetworkLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkVirtualNetwork) @@ -140,10 +140,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Scope: scope, Query: *network.Name, // List subnets in the virtual network }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -153,10 +149,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Scope: scope, Query: *network.Name, // List virtual network peerings in the virtual network }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Peering changes don't affect the Virtual Network itself - Out: true, // Virtual Network changes (especially deletion) affect peerings - }, }) // Link to DDoS protection plan @@ -177,10 +169,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: ddosPlanName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DDoS protection plan changes → Virtual Network protection affected (In: true) - Out: false, // If Virtual Network is deleted → DDoS protection plan remains (Out: false) - }, }) } } @@ -210,10 +198,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: nsgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NSG changes → Subnet security rules affected (In: true) - Out: false, // If Virtual Network is deleted → NSG remains (Out: false) - }, }) } } @@ -236,10 +220,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: routeTableName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Route Table changes → Subnet routing affected (In: true) - Out: false, // If Virtual Network is deleted → Route Table remains (Out: false) - }, }) } } @@ -262,10 +242,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: natGatewayName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NAT Gateway changes → Subnet outbound connectivity affected (In: true) - Out: false, // If Virtual Network is deleted → NAT Gateway remains (Out: false) - }, }) } } @@ -290,10 +266,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: privateEndpointName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Private Endpoint changes → Subnet connectivity affected (In: true) - Out: false, // If Virtual Network is deleted → Private Endpoint may become invalid (Out: false, but could be true) - }, }) } } @@ -322,10 +294,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: remoteVNetName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If remote VNet changes → Peering connectivity affected (In: true) - Out: true, // If this VNet changes → Remote VNet peering affected (Out: true) - }, }) } } @@ -349,10 +317,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: natGatewayName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NAT Gateway changes → VNet outbound connectivity affected (In: true) - Out: false, // If Virtual Network is deleted → NAT Gateway remains (Out: false) - }, }) } } diff --git a/sources/azure/manual/network-virtual-network_test.go b/sources/azure/manual/network-virtual-network_test.go index e230a831..880b3c33 100644 --- a/sources/azure/manual/network-virtual-network_test.go +++ b/sources/azure/manual/network-virtual-network_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -71,23 +71,13 @@ func TestNetworkVirtualNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vnetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // NetworkVirtualNetworkPeering link ExpectedType: azureshared.NetworkVirtualNetworkPeering.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vnetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -118,52 +108,27 @@ func TestNetworkVirtualNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vnetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.NetworkVirtualNetworkPeering.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vnetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.NetworkNatGateway.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nat-gateway", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "dns.internal", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) }) diff --git a/sources/azure/manual/network-zone.go b/sources/azure/manual/network-zone.go index aaec32ff..8bfea04e 100644 --- a/sources/azure/manual/network-zone.go +++ b/sources/azure/manual/network-zone.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -122,11 +122,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: zoneName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } @@ -150,12 +145,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: vnetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS zone depends on virtual network for registration - // If virtual network is deleted/modified, DNS zone registration may fail - In: true, - Out: false, - }, // Virtual network provides registration capability for the DNS zone }) } } @@ -182,12 +171,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: vnetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS zone depends on virtual network for resolution - // If virtual network is deleted/modified, DNS zone resolution may fail - In: true, - Out: false, - }, // Virtual network provides resolution capability for the DNS zone }) } } @@ -205,12 +188,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: zoneName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Record sets are child resources of the DNS zone - // Changes to record sets affect DNS resolution for the zone - In: true, - Out: true, - }, // Record sets are tightly coupled with the DNS zone; bidirectional dependency }) // Link to DNS names (standard library) from NameServers @@ -225,11 +202,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: *nameServer, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/network-zone_test.go b/sources/azure/manual/network-zone_test.go index e2c919de..c6a8d399 100644 --- a/sources/azure/manual/network-zone_test.go +++ b/sources/azure/manual/network-zone_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -72,67 +72,37 @@ func TestNetworkZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: zoneName, ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Virtual Network from RegistrationVirtualNetworks ExpectedType: azureshared.NetworkVirtualNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-reg-vnet", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Virtual Network from ResolutionVirtualNetworks ExpectedType: azureshared.NetworkVirtualNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-res-vnet", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS Record Set (child resource) ExpectedType: azureshared.NetworkDNSRecordSet.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: zoneName, ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS name server (standard library) ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "ns1.example.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS name server (standard library) ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "ns2.example.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/sql-database.go b/sources/azure/manual/sql-database.go index 34a1f32c..1de540a9 100644 --- a/sources/azure/manual/sql-database.go +++ b/sources/azure/manual/sql-database.go @@ -2,10 +2,13 @@ package manual import ( "context" + "errors" "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -85,10 +88,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: extractedServerName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes (especially deletion) affect the database's availability and configuration - Out: false, // Database changes don't affect the SQL Server itself - }, // SQL Database is a child resource that depends on its parent SQL Server }) } } @@ -103,10 +102,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: elasticPoolName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Elastic pool changes (especially deletion or resource configuration changes) affect the database's performance and availability - Out: false, // Database changes don't affect the elastic pool itself (though they may affect pool resource usage) - }, // SQL Database depends on its Elastic Pool for resource allocation and management }) } } @@ -124,10 +119,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(recoverableServerName, recoverableDatabaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Recoverable database deletion or unavailability affects the SQL Database's ability to restore from that point - Out: false, // SQL Database changes don't affect the recoverable database itself (it's a point-in-time snapshot) - }, // SQL Database depends on its recoverable database for disaster recovery and restore capabilities }) } } @@ -145,10 +136,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(restorableDroppedServerName, restorableDroppedDatabaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Restorable dropped database deletion/purge affects the SQL Database's ability to restore from that dropped database - Out: false, // SQL Database changes don't affect the restorable dropped database itself (it's already dropped) - }, // SQL Database depends on its restorable dropped database for restore capabilities after accidental deletion }) } } @@ -161,10 +148,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Recovery point deletion affects the SQL Database's ability to restore from that specific backup point - Out: false, // SQL Database changes don't affect the recovery point itself (it's a point-in-time snapshot) - }, // SQL Database depends on Recovery Services recovery points for backup and restore capabilities }) } @@ -180,10 +163,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(sourceServerName, sourceDatabaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Source database changes don't affect the copy (copy is independent after creation) - Out: false, // Copy database changes don't affect the source database - }, // Database copy is independent from its source after the copy operation completes }) } } @@ -205,10 +184,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(serverName, databaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Source database changes (especially deletion) affect the database's ability to restore - Out: false, // Database changes don't affect the source database itself - }, // SQL Database depends on the source SQL database for restore/recovery operations }) case azureshared.SourceResourceTypeSQLElasticPool: @@ -220,10 +195,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: elasticPoolName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Source elastic pool changes (especially deletion) affect the database's ability to restore - Out: false, // Database changes don't affect the source elastic pool itself - }, // SQL Database depends on the source SQL elastic pool for restore/recovery operations }) case azureshared.SourceResourceTypeUnknown: @@ -250,10 +221,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(failoverServerName, failoverGroupName), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Failover group deletion or failover affects the database's availability and replication - Out: false, // Database membership in the group doesn't change the failover group configuration - }, // SQL Database belongs to a Failover Group for high availability }) } } @@ -272,10 +239,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(locationName, ltrServerName, ltrDatabaseName, backupName), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // LTR backup deletion affects the database's ability to restore from that backup - Out: false, // SQL Database changes don't affect the LTR backup itself - }, // SQL Database depends on LTR backup for long-term retention restore }) } } @@ -297,10 +260,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: configName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Maintenance config changes affect when maintenance updates occur for the database - Out: false, // Database changes don't affect the maintenance configuration itself - }, // SQL Database uses Maintenance Configuration for update scheduling }) } } @@ -323,10 +282,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(vaultName, keyName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Key Vault Key deletion/rotation affects database encryption - Out: false, // Database changes don't affect the Key Vault Key - }, // SQL Database uses Key Vault Key for per-database CMK and encryption at rest }) } if database.Properties != nil && database.Properties.EncryptionProtector != nil && *database.Properties.EncryptionProtector != "" { @@ -359,10 +314,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // User Assigned Identity deletion affects database identity and CMK access - Out: false, // Database changes don't affect the User Assigned Identity - }, // SQL Database uses User Assigned Identity for Azure AD auth and CMK }) } } @@ -377,10 +328,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(serverName, databaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Schema changes don't affect the parent database resource - Out: true, // Database deletion removes all schemas - }, // Database Schemas are child resources of the SQL Database }) return sdpItem, nil @@ -431,6 +378,40 @@ func (s sqlDatabaseWrapper) Search(ctx context.Context, scope string, queryParts return items, nil } +func (s sqlDatabaseWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: serverName"), scope, s.Type())) + return + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, database := range page.Value { + if database.Name == nil { + continue + } + item, sdpErr := s.azureSqlDatabaseToSDPItem(database, serverName, *database.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s sqlDatabaseWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/sql-database_test.go b/sources/azure/manual/sql-database_test.go index 4e08fba1..e043a58f 100644 --- a/sources/azure/manual/sql-database_test.go +++ b/sources/azure/manual/sql-database_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -120,23 +120,13 @@ func TestSqlDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // SQLDatabaseSchema child resource link ExpectedType: azureshared.SQLDatabaseSchema.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: shared.CompositeLookupKey(serverName, databaseName), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -170,34 +160,19 @@ func TestSqlDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // SQLElasticPool link ExpectedType: azureshared.SQLElasticPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // SQLDatabaseSchema child resource link ExpectedType: azureshared.SQLDatabaseSchema.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: shared.CompositeLookupKey(serverName, databaseName), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/sql-server.go b/sources/azure/manual/sql-server.go index c6f7b3f0..75027a92 100644 --- a/sources/azure/manual/sql-server.go +++ b/sources/azure/manual/sql-server.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -144,10 +144,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes (especially deletion) affect databases - Out: false, // Database changes don't affect the SQL Server itself - }, // SQL Databases are child resources that depend on their parent SQL Server }) // Link to Elastic Pools (child resource) @@ -160,10 +156,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect elastic pools - Out: false, // Elastic pool changes don't affect the SQL Server itself - }, // SQL Elastic Pools are child resources that depend on their parent SQL Server }) // Link to Firewall Rules (child resource) @@ -176,10 +168,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect firewall rules - Out: true, // Firewall rule changes affect server connectivity - }, // SQL Server Firewall Rules are child resources that control server access }) // Link to Virtual Network Rules (child resource) @@ -192,10 +180,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect virtual network rules - Out: true, // Virtual network rule changes affect server connectivity - }, // SQL Server Virtual Network Rules are child resources that control server access }) // Link to Server Keys (child resource) @@ -208,10 +192,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect server keys - Out: true, // Server key changes (especially deletion) affect server encryption and availability - }, // SQL Server Keys are child resources used for encryption }) // Link to Failover Groups (child resource) @@ -224,10 +204,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect failover groups - Out: true, // Failover group changes affect server availability and failover behavior - }, // SQL Server Failover Groups are child resources that manage high availability }) // Link to Administrators (child resource) @@ -240,10 +216,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect administrators - Out: true, // Administrator changes affect server authentication and access control - }, // SQL Server Administrators are child resources that control authentication }) // Link to Sync Groups (child resource) @@ -256,10 +228,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect sync groups - Out: false, // Sync group changes don't affect the SQL Server itself - }, // SQL Server Sync Groups are child resources for data synchronization }) // Link to Sync Agents (child resource) @@ -272,10 +240,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect sync agents - Out: false, // Sync agent changes don't affect the SQL Server itself - }, // SQL Server Sync Agents are child resources for data synchronization }) // Link to Private Endpoint Connections (child resource) @@ -288,10 +252,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect private endpoint connections - Out: true, // Private endpoint connection changes affect server network connectivity - }, // SQL Server Private Endpoint Connections are child resources that manage private network access }) // Link to Network Private Endpoints (external resources) from PrivateEndpointConnections @@ -315,10 +275,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: privateEndpointName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes (deletion, network configuration) affect the SQL Server's private connectivity - Out: true, // SQL Server deletion or configuration changes may affect the private endpoint's connection state - }, // Private endpoints are tightly coupled to the SQL Server - changes affect connectivity }) } } @@ -335,10 +291,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect auditing settings - Out: true, // Auditing setting changes affect server security and compliance - }, // SQL Server Auditing Settings are child resources that control audit logging }) // Link to Security Alert Policies (child resource) @@ -351,10 +303,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect security alert policies - Out: true, // Security alert policy changes affect server security monitoring - }, // SQL Server Security Alert Policies are child resources that control threat detection }) // Link to Vulnerability Assessments (child resource) @@ -367,10 +315,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect vulnerability assessments - Out: true, // Vulnerability assessment changes affect server security scanning - }, // SQL Server Vulnerability Assessments are child resources that control security scanning }) // Link to Encryption Protector (child resource) @@ -383,10 +327,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect encryption protector - Out: true, // Encryption protector changes affect server encryption and data access - }, // SQL Server Encryption Protector is a child resource that controls encryption }) // Link to Blob Auditing Policies (child resource) @@ -399,10 +339,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect blob auditing policies - Out: true, // Blob auditing policy changes affect server audit logging - }, // SQL Server Blob Auditing Policies are child resources that control blob audit logging }) // Link to Automatic Tuning (child resource) @@ -415,10 +351,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect automatic tuning - Out: true, // Automatic tuning changes affect server performance optimization - }, // SQL Server Automatic Tuning is a child resource that controls performance optimization }) // Link to Advanced Threat Protection Settings (child resource) @@ -431,10 +363,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect advanced threat protection settings - Out: true, // Advanced threat protection setting changes affect server security - }, // SQL Server Advanced Threat Protection Settings are child resources that control threat protection }) // Link to DNS Aliases (child resource) @@ -447,10 +375,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect DNS aliases - Out: true, // DNS alias changes affect server connectivity and routing - }, // SQL Server DNS Aliases are child resources that provide alternate DNS names }) // Link to Server Usages (child resource) @@ -463,10 +387,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect usage metrics - Out: false, // Usage metrics are read-only and don't affect the server - }, // SQL Server Usages are child resources that provide usage metrics }) // Link to Server Operations (child resource) @@ -479,10 +399,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect operations history - Out: false, // Operations history is read-only and doesn't affect the server - }, // SQL Server Operations are child resources that provide operation history }) // Link to Server Advisors (child resource) @@ -495,10 +411,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect advisors - Out: false, // Advisor recommendations don't affect the server until applied - }, // SQL Server Advisors are child resources that provide optimization recommendations }) // Link to Backup Long-Term Retention Policies (child resource) @@ -511,10 +423,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect backup retention policies - Out: true, // Backup retention policy changes affect backup management and storage - }, // SQL Server Backup Long-Term Retention Policies are child resources that control backup retention }) // Link to DevOps Audit Settings (child resource) @@ -527,10 +435,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect DevOps audit settings - Out: true, // DevOps audit setting changes affect server audit logging - }, // SQL Server DevOps Audit Settings are child resources that control DevOps audit logging }) // Link to Server Trust Groups (child resource) @@ -543,10 +447,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect trust groups - Out: true, // Trust group changes affect server trust relationships - }, // SQL Server Trust Groups are child resources that manage trust relationships }) // Link to Outbound Firewall Rules (child resource) @@ -559,10 +459,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect outbound firewall rules - Out: true, // Outbound firewall rule changes affect server outbound connectivity - }, // SQL Server Outbound Firewall Rules are child resources that control outbound access }) // Link to Private Link Resources (child resource) @@ -575,10 +471,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect private link resources - Out: false, // Private link resources are metadata about available private endpoints - }, // SQL Server Private Link Resources are child resources that provide private link metadata }) // Link to Long Term Retention Backups (child resource) @@ -591,10 +483,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // LTR backup changes don't affect the SQL Server itself - Out: true, // SQL Server changes (deletion) affect LTR backups - }, // SQL Server Long Term Retention Backups are child resources that can be listed by server }) } @@ -620,10 +508,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: identityName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Managed identity deletion/modification affects server authentication and operations - Out: false, // Server changes don't affect the managed identity itself - }, // SQL Server depends on managed identity for authentication }) processedIdentityIDs[*server.Properties.PrimaryUserAssignedIdentityID] = true } @@ -651,10 +535,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: identityName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Managed identity deletion/modification affects server authentication and operations - Out: false, // Server changes don't affect the managed identity itself - }, // SQL Server depends on managed identity for authentication }) processedIdentityIDs[identityResourceID] = true } @@ -682,10 +562,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: vaultName, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Key Vault changes (key deletion, rotation, access policy) affect the SQL Server's encryption - Out: false, // SQL Server changes don't directly affect the Key Vault - }, // SQL Server depends on Key Vault for customer-managed encryption keys - key changes impact encryption/decryption }) } } @@ -700,10 +576,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: *server.Properties.FullyQualifiedDomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect server connectivity - Out: true, // DNS names are always linked bidirectionally - }, // DNS names are shared resources that affect multiple entities }) } } diff --git a/sources/azure/manual/sql-server_test.go b/sources/azure/manual/sql-server_test.go index 255126f6..d2165407 100644 --- a/sources/azure/manual/sql-server_test.go +++ b/sources/azure/manual/sql-server_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -118,283 +118,143 @@ func TestSqlServer(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLElasticPool.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerFirewallRule.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerVirtualNetworkRule.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerKey.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerFailoverGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAdministrator.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerSyncGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerSyncAgent.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerPrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAuditingSetting.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerSecurityAlertPolicy.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerVulnerabilityAssessment.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerEncryptionProtector.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerBlobAuditingPolicy.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAutomaticTuning.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAdvancedThreatProtectionSetting.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerDnsAlias.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerUsage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerOperation.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAdvisor.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerBackupLongTermRetentionPolicy.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerDevOpsAuditSetting.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerTrustGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerOutboundFirewallRule.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerPrivateLinkResource.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLLongTermRetentionBackup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // DNS name link (from FullyQualifiedDomainName) ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName + ".database.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -562,10 +422,6 @@ func TestSqlServer(t *testing.T) { if link.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected NetworkPrivateEndpoint method GET, got %v", link.GetQuery().GetMethod()) } - if link.GetBlastPropagation().GetIn() != true || link.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected NetworkPrivateEndpoint BlastPropagation In=true, Out=true, got In=%v, Out=%v", - link.GetBlastPropagation().GetIn(), link.GetBlastPropagation().GetOut()) - } } } @@ -625,10 +481,6 @@ func TestSqlServer(t *testing.T) { if link.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected KeyVault method GET, got %v", link.GetQuery().GetMethod()) } - if link.GetBlastPropagation().GetIn() != true || link.GetBlastPropagation().GetOut() != false { - t.Errorf("Expected KeyVault BlastPropagation In=true, Out=false, got In=%v, Out=%v", - link.GetBlastPropagation().GetIn(), link.GetBlastPropagation().GetOut()) - } } } diff --git a/sources/azure/manual/storage-account.go b/sources/azure/manual/storage-account.go index b6465eb6..5ab37a6e 100644 --- a/sources/azure/manual/storage-account.go +++ b/sources/azure/manual/storage-account.go @@ -5,16 +5,15 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" ) - var StorageAccountLookupByName = shared.NewItemTypeLookup("name", azureshared.StorageAccount) type storageAccountWrapper struct { @@ -89,7 +88,7 @@ func (s storageAccountWrapper) ListStream(ctx context.Context, stream discovery. cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) } -} + } } func (s storageAccountWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { @@ -144,10 +143,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Storage account is NOT affected if blob containers change - Out: true, // Blob containers ARE affected if storage account changes/deletes - }, }) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -157,10 +152,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Storage account is NOT affected if file shares change - Out: true, // File shares ARE affected if storage account changes/deletes - }, }) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -170,10 +161,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Storage account is NOT affected if tables change - Out: true, // Tables ARE affected if storage account changes/deletes - }, }) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -183,10 +170,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Storage account is NOT affected if queues change - Out: true, // Queues ARE affected if storage account changes/deletes - }, }) // Link to Private Endpoint Connections (child resource) @@ -199,12 +182,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint connections are child resources of the storage account - // Changes to storage account affect connections, and connection state affects storage access - In: true, - Out: true, - }, }) // Link to User Assigned Managed Identities (external resources) @@ -225,12 +202,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Storage account depends on managed identity for authentication - // If identity is deleted/modified, storage account operations may fail - In: true, - Out: false, - }, }) } } @@ -258,12 +229,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: vaultName, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - // Storage account depends on Key Vault for customer-managed encryption keys - // If Key Vault is deleted/modified or key is rotated, storage account encryption may be affected - In: true, - Out: false, - }, }) } } @@ -288,12 +253,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Storage account depends on managed identity for encryption key access - // If identity is deleted/modified, storage account encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -327,12 +286,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: query, Scope: scope, // Use the subnet's scope, not the storage account's scope }, - BlastPropagation: &sdp.BlastPropagation{ - // Storage account depends on subnet for network access control - // If subnet is deleted/modified, storage account network access may be affected - In: true, - Out: false, - }, }) } } @@ -350,11 +303,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: *ipRule.IPAddressOrRange, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -371,11 +319,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: *ipRule.IPAddressOrRange, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -401,12 +344,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: privateEndpointName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint connection is tightly coupled with the storage account - // Changes to either affect the other - In: true, - Out: true, - }, }) } } @@ -439,11 +376,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -476,11 +408,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -496,11 +423,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: *account.Properties.CustomDomain.Name, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } diff --git a/sources/azure/manual/storage-account_test.go b/sources/azure/manual/storage-account_test.go index 4c595eed..99a77918 100644 --- a/sources/azure/manual/storage-account_test.go +++ b/sources/azure/manual/storage-account_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -69,100 +69,55 @@ func TestStorageAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Storage file share link ExpectedType: azureshared.StorageFileShare.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Storage table link ExpectedType: azureshared.StorageTable.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Storage queue link ExpectedType: azureshared.StorageQueue.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Storage private endpoint connection link (child resource) ExpectedType: azureshared.StoragePrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS link from PrimaryEndpoints.Blob ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName + ".blob.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS link from PrimaryEndpoints.Queue ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName + ".queue.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS link from PrimaryEndpoints.Table ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName + ".table.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS link from PrimaryEndpoints.File ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName + ".file.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/storage-blob-container.go b/sources/azure/manual/storage-blob-container.go index 7c424c87..d123ad3b 100644 --- a/sources/azure/manual/storage-blob-container.go +++ b/sources/azure/manual/storage-blob-container.go @@ -6,7 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -107,6 +109,49 @@ func (s storageBlobContainerWrapper) Search(ctx context.Context, scope string, q return items, nil } +func (s storageBlobContainerWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(fmt.Errorf("queryParts must be 1 query part: storageAccountName, got %d", len(queryParts)), scope, s.Type())) + return + } + storageAccountName := queryParts[0] + if storageAccountName == "" { + stream.SendError(azureshared.QueryError(fmt.Errorf("storageAccountName cannot be empty"), scope, s.Type())) + return + } + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, container := range page.Value { + if container.Name == nil { + continue + } + item, sdpErr := s.azureBlobContainerToSDPItem(&armstorage.BlobContainer{ + ID: container.ID, + Name: container.Name, + Type: container.Type, + ContainerProperties: container.Properties, + Etag: container.Etag, + }, storageAccountName, *container.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s storageBlobContainerWrapper) GetLookups() sources.ItemTypeLookups { return sources.ItemTypeLookups{ StorageAccountLookupByName, @@ -157,10 +202,6 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) // Link to DNS name (standard library) from blob container URI @@ -176,10 +217,6 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS name is unavailable → blob container becomes inaccessible (In: true) - Out: true, // If blob container is deleted → DNS name may still be used by other resources (Out: true) - }, // Blob container depends on DNS name for endpoint resolution }) } @@ -192,10 +229,6 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms Query: blobContainerURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → blob container becomes inaccessible (In: true) - Out: true, // If blob container is deleted → HTTP endpoint may still be used by other resources (Out: true) - }, // Blob container depends on HTTP endpoint for access }) } @@ -208,10 +241,6 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms Query: shared.CompositeLookupKey(storageAccountName, *container.ContainerProperties.DefaultEncryptionScope), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If encryption scope is removed or changed → container's default encryption is affected - Out: false, // Container deletion does not affect the encryption scope - }, }) } @@ -221,9 +250,10 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms func (s storageBlobContainerWrapper) TerraformMappings() []*sdp.TerraformMapping { return []*sdp.TerraformMapping{ { - TerraformMethod: sdp.QueryMethod_GET, + TerraformMethod: sdp.QueryMethod_SEARCH, // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_container - TerraformQueryMap: "azurerm_storage_container.name", + // Terraform uses: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/blobServices/default/containers/{container} + TerraformQueryMap: "azurerm_storage_container.id", }, } } diff --git a/sources/azure/manual/storage-blob-container_test.go b/sources/azure/manual/storage-blob-container_test.go index 9c83b4d0..86a391be 100644 --- a/sources/azure/manual/storage-blob-container_test.go +++ b/sources/azure/manual/storage-blob-container_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -130,12 +130,6 @@ func TestStorageBlobContainer(t *testing.T) { if linkedQuery.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected StorageAccount linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected StorageAccount BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected StorageAccount BlastPropagation.Out to be false") - } case "dns": hasDNSLink = true if linkedQuery.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { @@ -213,12 +207,6 @@ func TestStorageBlobContainer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != subscriptionID+"."+resourceGroup { t.Errorf("Expected StorageEncryptionScope scope %s, got %s", subscriptionID+"."+resourceGroup, linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected StorageEncryptionScope BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected StorageEncryptionScope BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/manual/storage-fileshare.go b/sources/azure/manual/storage-fileshare.go index ed3a3366..21c8d3cb 100644 --- a/sources/azure/manual/storage-fileshare.go +++ b/sources/azure/manual/storage-fileshare.go @@ -2,9 +2,12 @@ package manual import ( "context" + "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -115,6 +118,46 @@ func (s storageFileShareWrapper) Search(ctx context.Context, scope string, query return items, nil } +func (s storageFileShareWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: storageAccountName"), scope, s.Type())) + return + } + storageAccountName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, fileShare := range page.Value { + if fileShare.Name == nil { + continue + } + item, sdpErr := s.azureFileShareToSDPItem(&armstorage.FileShare{ + ID: fileShare.ID, + Name: fileShare.Name, + Type: fileShare.Type, + FileShareProperties: fileShare.Properties, + Etag: fileShare.Etag, + }, storageAccountName, *fileShare.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s storageFileShareWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { @@ -154,12 +197,6 @@ func (s storageFileShareWrapper) azureFileShareToSDPItem(fileShare *armstorage.F Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // File Share depends on Storage Account (parent); deletion/change of Storage Account affects File Share. - // Storage Account is not affected when a child File Share is deleted. - In: true, - Out: false, - }, }) return sdpItem, nil @@ -168,9 +205,10 @@ func (s storageFileShareWrapper) azureFileShareToSDPItem(fileShare *armstorage.F func (s storageFileShareWrapper) TerraformMappings() []*sdp.TerraformMapping { return []*sdp.TerraformMapping{ { - TerraformMethod: sdp.QueryMethod_GET, + TerraformMethod: sdp.QueryMethod_SEARCH, // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_share - TerraformQueryMap: "azurerm_storage_share.name", + // Terraform uses: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/fileServices/default/shares/{share} + TerraformQueryMap: "azurerm_storage_share.id", }, } } diff --git a/sources/azure/manual/storage-fileshare_test.go b/sources/azure/manual/storage-fileshare_test.go index ab0f5c91..a29503b0 100644 --- a/sources/azure/manual/storage-fileshare_test.go +++ b/sources/azure/manual/storage-fileshare_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -128,12 +128,6 @@ func TestStorageFileShare(t *testing.T) { if linkedQuery.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } }) }) diff --git a/sources/azure/manual/storage-queues.go b/sources/azure/manual/storage-queues.go index f3121421..0d72934b 100644 --- a/sources/azure/manual/storage-queues.go +++ b/sources/azure/manual/storage-queues.go @@ -2,9 +2,12 @@ package manual import ( "context" + "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -80,10 +83,6 @@ func (s storageQueuesWrapper) azureQueueToSDPItem(queue *armstorage.Queue, stora Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Queue depends on storage account; account deletion/change affects the queue. - Out: false, // Storage account is not affected by queue create/update/delete. - }, }) return sdpItem, nil @@ -154,6 +153,47 @@ func (s storageQueuesWrapper) Search(ctx context.Context, scope string, queryPar return items, nil } +func (s storageQueuesWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: storageAccountName"), scope, s.Type())) + return + } + storageAccountName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, queue := range page.Value { + if queue.Name == nil || queue.QueueProperties == nil { + continue + } + item, sdpErr := s.azureQueueToSDPItem(&armstorage.Queue{ + ID: queue.ID, + Name: queue.Name, + Type: queue.Type, + QueueProperties: &armstorage.QueueProperties{ + Metadata: queue.QueueProperties.Metadata, + }, + }, storageAccountName, *queue.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s storageQueuesWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/storage-queues_test.go b/sources/azure/manual/storage-queues_test.go index 2e003bcc..ad631a15 100644 --- a/sources/azure/manual/storage-queues_test.go +++ b/sources/azure/manual/storage-queues_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -129,12 +129,6 @@ func TestStorageQueues(t *testing.T) { if linkedQuery.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } }) }) diff --git a/sources/azure/manual/storage-table.go b/sources/azure/manual/storage-table.go index f8db9364..01defcb4 100644 --- a/sources/azure/manual/storage-table.go +++ b/sources/azure/manual/storage-table.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -86,10 +88,6 @@ func (s storageTablesWrapper) azureTableToSDPItem(table *armstorage.Table, stora Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Table is affected if parent storage account changes or is deleted - Out: false, // Table changes/deletes do not affect the storage account - }, }) return sdpItem, nil @@ -157,6 +155,49 @@ func (s storageTablesWrapper) Search(ctx context.Context, scope string, queryPar return items, nil } +func (s storageTablesWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(fmt.Errorf("queryParts must be 1 query part: storageAccountName, got %d", len(queryParts)), scope, s.Type())) + return + } + storageAccountName := queryParts[0] + if storageAccountName == "" { + stream.SendError(azureshared.QueryError(fmt.Errorf("storageAccountName cannot be empty"), scope, s.Type())) + return + } + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, table := range page.Value { + if table.Name == nil { + continue + } + item, sdpErr := s.azureTableToSDPItem(&armstorage.Table{ + ID: table.ID, + Name: table.Name, + Type: table.Type, + TableProperties: table.TableProperties, + }, storageAccountName, *table.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s storageTablesWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/storage-table_test.go b/sources/azure/manual/storage-table_test.go index 0ecc47cd..86d1b2ad 100644 --- a/sources/azure/manual/storage-table_test.go +++ b/sources/azure/manual/storage-table_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" @@ -129,12 +129,6 @@ func TestStorageTables(t *testing.T) { if linkedQuery.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } }) }) diff --git a/sources/azure/proc/proc.go b/sources/azure/proc/proc.go index b0c78d2e..2fddfca4 100644 --- a/sources/azure/proc/proc.go +++ b/sources/azure/proc/proc.go @@ -10,9 +10,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/viper" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" // TODO: Uncomment when Azure dynamic adapters are implemented // "github.com/overmindtech/cli/sources/azure/dynamic" diff --git a/sources/azure/proc/proc_test.go b/sources/azure/proc/proc_test.go index 2f5c1533..ddcaf803 100644 --- a/sources/azure/proc/proc_test.go +++ b/sources/azure/proc/proc_test.go @@ -6,7 +6,7 @@ import ( // TODO: Uncomment when Azure dynamic adapters are implemented // _ "github.com/overmindtech/cli/sources/azure/dynamic" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" azureshared "github.com/overmindtech/cli/sources/azure/shared" ) diff --git a/sources/azure/shared/adapter-meta.go b/sources/azure/shared/adapter-meta.go index cddb8bac..32012274 100644 --- a/sources/azure/shared/adapter-meta.go +++ b/sources/azure/shared/adapter-meta.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/azure/shared/base.go b/sources/azure/shared/base.go index e71d1554..40e9605e 100644 --- a/sources/azure/shared/base.go +++ b/sources/azure/shared/base.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/azure/shared/errors.go b/sources/azure/shared/errors.go index 3366db93..abc77b06 100644 --- a/sources/azure/shared/errors.go +++ b/sources/azure/shared/errors.go @@ -4,7 +4,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // QueryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index 1cdd82a3..41b2f74d 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -7,22 +7,29 @@ import "github.com/overmindtech/cli/sources/shared" // to create unique item type identifiers following the pattern: azure-{api}-{resource} var ( // Compute item types - ComputeVirtualMachine = shared.NewItemType(Azure, Compute, VirtualMachine) - ComputeDisk = shared.NewItemType(Azure, Compute, Disk) - ComputeAvailabilitySet = shared.NewItemType(Azure, Compute, AvailabilitySet) - ComputeVirtualMachineExtension = shared.NewItemType(Azure, Compute, VirtualMachineExtension) - ComputeVirtualMachineRunCommand = shared.NewItemType(Azure, Compute, VirtualMachineRunCommand) - ComputeVirtualMachineScaleSet = shared.NewItemType(Azure, Compute, VirtualMachineScaleSet) - ComputeDiskEncryptionSet = shared.NewItemType(Azure, Compute, DiskEncryptionSet) - ComputeProximityPlacementGroup = shared.NewItemType(Azure, Compute, ProximityPlacementGroup) - ComputeDedicatedHostGroup = shared.NewItemType(Azure, Compute, DedicatedHostGroup) - ComputeCapacityReservationGroup = shared.NewItemType(Azure, Compute, CapacityReservationGroup) - ComputeImage = shared.NewItemType(Azure, Compute, Image) - ComputeSnapshot = shared.NewItemType(Azure, Compute, Snapshot) - ComputeDiskAccess = shared.NewItemType(Azure, Compute, DiskAccess) - ComputeSharedGalleryImage = shared.NewItemType(Azure, Compute, SharedGalleryImage) - ComputeCommunityGalleryImage = shared.NewItemType(Azure, Compute, CommunityGalleryImage) - ComputeSharedGalleryApplicationVersion = shared.NewItemType(Azure, Compute, SharedGalleryApplicationVersion) + ComputeVirtualMachine = shared.NewItemType(Azure, Compute, VirtualMachine) + ComputeDisk = shared.NewItemType(Azure, Compute, Disk) + ComputeAvailabilitySet = shared.NewItemType(Azure, Compute, AvailabilitySet) + ComputeVirtualMachineExtension = shared.NewItemType(Azure, Compute, VirtualMachineExtension) + ComputeVirtualMachineRunCommand = shared.NewItemType(Azure, Compute, VirtualMachineRunCommand) + ComputeVirtualMachineScaleSet = shared.NewItemType(Azure, Compute, VirtualMachineScaleSet) + ComputeDiskEncryptionSet = shared.NewItemType(Azure, Compute, DiskEncryptionSet) + ComputeProximityPlacementGroup = shared.NewItemType(Azure, Compute, ProximityPlacementGroup) + ComputeDedicatedHostGroup = shared.NewItemType(Azure, Compute, DedicatedHostGroup) + ComputeDedicatedHost = shared.NewItemType(Azure, Compute, DedicatedHost) + ComputeCapacityReservationGroup = shared.NewItemType(Azure, Compute, CapacityReservationGroup) + ComputeCapacityReservation = shared.NewItemType(Azure, Compute, CapacityReservation) + ComputeImage = shared.NewItemType(Azure, Compute, Image) + ComputeSnapshot = shared.NewItemType(Azure, Compute, Snapshot) + ComputeDiskAccess = shared.NewItemType(Azure, Compute, DiskAccess) + ComputeDiskAccessPrivateEndpointConnection = shared.NewItemType(Azure, Compute, DiskAccessPrivateEndpointConnection) + ComputeSharedGalleryImage = shared.NewItemType(Azure, Compute, SharedGalleryImage) + ComputeSharedGallery = shared.NewItemType(Azure, Compute, SharedGallery) + ComputeCommunityGalleryImage = shared.NewItemType(Azure, Compute, CommunityGalleryImage) + ComputeGalleryApplication = shared.NewItemType(Azure, Compute, GalleryApplication) + ComputeGalleryApplicationVersion = shared.NewItemType(Azure, Compute, GalleryApplicationVersion) + ComputeGalleryImage = shared.NewItemType(Azure, Compute, GalleryImage) + ComputeGallery = shared.NewItemType(Azure, Compute, Gallery) // Network item types NetworkVirtualNetwork = shared.NewItemType(Azure, Network, VirtualNetwork) diff --git a/sources/azure/shared/mocks/mock_capacity_reservation_groups_client.go b/sources/azure/shared/mocks/mock_capacity_reservation_groups_client.go new file mode 100644 index 00000000..bc4ccdad --- /dev/null +++ b/sources/azure/shared/mocks/mock_capacity_reservation_groups_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: capacity-reservation-groups-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_capacity_reservation_groups_client.go -package=mocks -source=capacity-reservation-groups-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockCapacityReservationGroupsClient is a mock of CapacityReservationGroupsClient interface. +type MockCapacityReservationGroupsClient struct { + ctrl *gomock.Controller + recorder *MockCapacityReservationGroupsClientMockRecorder + isgomock struct{} +} + +// MockCapacityReservationGroupsClientMockRecorder is the mock recorder for MockCapacityReservationGroupsClient. +type MockCapacityReservationGroupsClientMockRecorder struct { + mock *MockCapacityReservationGroupsClient +} + +// NewMockCapacityReservationGroupsClient creates a new mock instance. +func NewMockCapacityReservationGroupsClient(ctrl *gomock.Controller) *MockCapacityReservationGroupsClient { + mock := &MockCapacityReservationGroupsClient{ctrl: ctrl} + mock.recorder = &MockCapacityReservationGroupsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCapacityReservationGroupsClient) EXPECT() *MockCapacityReservationGroupsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockCapacityReservationGroupsClient) Get(ctx context.Context, resourceGroupName, capacityReservationGroupName string, options *armcompute.CapacityReservationGroupsClientGetOptions) (armcompute.CapacityReservationGroupsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, capacityReservationGroupName, options) + ret0, _ := ret[0].(armcompute.CapacityReservationGroupsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockCapacityReservationGroupsClientMockRecorder) Get(ctx, resourceGroupName, capacityReservationGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCapacityReservationGroupsClient)(nil).Get), ctx, resourceGroupName, capacityReservationGroupName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockCapacityReservationGroupsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions) clients.CapacityReservationGroupsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.CapacityReservationGroupsPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockCapacityReservationGroupsClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockCapacityReservationGroupsClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/mocks/mock_dedicated_host_groups_client.go b/sources/azure/shared/mocks/mock_dedicated_host_groups_client.go new file mode 100644 index 00000000..67632dc2 --- /dev/null +++ b/sources/azure/shared/mocks/mock_dedicated_host_groups_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: dedicated-host-groups-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_dedicated_host_groups_client.go -package=mocks -source=dedicated-host-groups-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockDedicatedHostGroupsClient is a mock of DedicatedHostGroupsClient interface. +type MockDedicatedHostGroupsClient struct { + ctrl *gomock.Controller + recorder *MockDedicatedHostGroupsClientMockRecorder + isgomock struct{} +} + +// MockDedicatedHostGroupsClientMockRecorder is the mock recorder for MockDedicatedHostGroupsClient. +type MockDedicatedHostGroupsClientMockRecorder struct { + mock *MockDedicatedHostGroupsClient +} + +// NewMockDedicatedHostGroupsClient creates a new mock instance. +func NewMockDedicatedHostGroupsClient(ctrl *gomock.Controller) *MockDedicatedHostGroupsClient { + mock := &MockDedicatedHostGroupsClient{ctrl: ctrl} + mock.recorder = &MockDedicatedHostGroupsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDedicatedHostGroupsClient) EXPECT() *MockDedicatedHostGroupsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDedicatedHostGroupsClient) Get(ctx context.Context, resourceGroupName, dedicatedHostGroupName string, options *armcompute.DedicatedHostGroupsClientGetOptions) (armcompute.DedicatedHostGroupsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, dedicatedHostGroupName, options) + ret0, _ := ret[0].(armcompute.DedicatedHostGroupsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDedicatedHostGroupsClientMockRecorder) Get(ctx, resourceGroupName, dedicatedHostGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDedicatedHostGroupsClient)(nil).Get), ctx, resourceGroupName, dedicatedHostGroupName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockDedicatedHostGroupsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DedicatedHostGroupsClientListByResourceGroupOptions) clients.DedicatedHostGroupsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.DedicatedHostGroupsPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockDedicatedHostGroupsClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockDedicatedHostGroupsClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/mocks/mock_disk_accesses_client.go b/sources/azure/shared/mocks/mock_disk_accesses_client.go new file mode 100644 index 00000000..f8a14448 --- /dev/null +++ b/sources/azure/shared/mocks/mock_disk_accesses_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: disk-accesses-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_disk_accesses_client.go -package=mocks -source=disk-accesses-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockDiskAccessesClient is a mock of DiskAccessesClient interface. +type MockDiskAccessesClient struct { + ctrl *gomock.Controller + recorder *MockDiskAccessesClientMockRecorder + isgomock struct{} +} + +// MockDiskAccessesClientMockRecorder is the mock recorder for MockDiskAccessesClient. +type MockDiskAccessesClientMockRecorder struct { + mock *MockDiskAccessesClient +} + +// NewMockDiskAccessesClient creates a new mock instance. +func NewMockDiskAccessesClient(ctrl *gomock.Controller) *MockDiskAccessesClient { + mock := &MockDiskAccessesClient{ctrl: ctrl} + mock.recorder = &MockDiskAccessesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDiskAccessesClient) EXPECT() *MockDiskAccessesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDiskAccessesClient) Get(ctx context.Context, resourceGroupName, diskAccessName string, options *armcompute.DiskAccessesClientGetOptions) (armcompute.DiskAccessesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, diskAccessName, options) + ret0, _ := ret[0].(armcompute.DiskAccessesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDiskAccessesClientMockRecorder) Get(ctx, resourceGroupName, diskAccessName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDiskAccessesClient)(nil).Get), ctx, resourceGroupName, diskAccessName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockDiskAccessesClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DiskAccessesClientListByResourceGroupOptions) clients.DiskAccessesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.DiskAccessesPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockDiskAccessesClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockDiskAccessesClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/mocks/mock_galleries_client.go b/sources/azure/shared/mocks/mock_galleries_client.go new file mode 100644 index 00000000..fc58e848 --- /dev/null +++ b/sources/azure/shared/mocks/mock_galleries_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: galleries-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_galleries_client.go -package=mocks -source=galleries-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleriesClient is a mock of GalleriesClient interface. +type MockGalleriesClient struct { + ctrl *gomock.Controller + recorder *MockGalleriesClientMockRecorder + isgomock struct{} +} + +// MockGalleriesClientMockRecorder is the mock recorder for MockGalleriesClient. +type MockGalleriesClientMockRecorder struct { + mock *MockGalleriesClient +} + +// NewMockGalleriesClient creates a new mock instance. +func NewMockGalleriesClient(ctrl *gomock.Controller) *MockGalleriesClient { + mock := &MockGalleriesClient{ctrl: ctrl} + mock.recorder = &MockGalleriesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleriesClient) EXPECT() *MockGalleriesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleriesClient) Get(ctx context.Context, resourceGroupName, galleryName string, options *armcompute.GalleriesClientGetOptions) (armcompute.GalleriesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, options) + ret0, _ := ret[0].(armcompute.GalleriesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleriesClientMockRecorder) Get(ctx, resourceGroupName, galleryName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleriesClient)(nil).Get), ctx, resourceGroupName, galleryName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockGalleriesClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.GalleriesClientListByResourceGroupOptions) clients.GalleriesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.GalleriesPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockGalleriesClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockGalleriesClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/mocks/mock_gallery_application_versions_client.go b/sources/azure/shared/mocks/mock_gallery_application_versions_client.go new file mode 100644 index 00000000..e88ce82b --- /dev/null +++ b/sources/azure/shared/mocks/mock_gallery_application_versions_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: gallery-application-versions-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_gallery_application_versions_client.go -package=mocks -source=gallery-application-versions-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleryApplicationVersionsClient is a mock of GalleryApplicationVersionsClient interface. +type MockGalleryApplicationVersionsClient struct { + ctrl *gomock.Controller + recorder *MockGalleryApplicationVersionsClientMockRecorder + isgomock struct{} +} + +// MockGalleryApplicationVersionsClientMockRecorder is the mock recorder for MockGalleryApplicationVersionsClient. +type MockGalleryApplicationVersionsClientMockRecorder struct { + mock *MockGalleryApplicationVersionsClient +} + +// NewMockGalleryApplicationVersionsClient creates a new mock instance. +func NewMockGalleryApplicationVersionsClient(ctrl *gomock.Controller) *MockGalleryApplicationVersionsClient { + mock := &MockGalleryApplicationVersionsClient{ctrl: ctrl} + mock.recorder = &MockGalleryApplicationVersionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryApplicationVersionsClient) EXPECT() *MockGalleryApplicationVersionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryApplicationVersionsClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) + ret0, _ := ret[0].(armcompute.GalleryApplicationVersionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryApplicationVersionsClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) +} + +// NewListByGalleryApplicationPager mocks base method. +func (m *MockGalleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) clients.GalleryApplicationVersionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryApplicationPager", resourceGroupName, galleryName, galleryApplicationName, options) + ret0, _ := ret[0].(clients.GalleryApplicationVersionsPager) + return ret0 +} + +// NewListByGalleryApplicationPager indicates an expected call of NewListByGalleryApplicationPager. +func (mr *MockGalleryApplicationVersionsClientMockRecorder) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryApplicationPager", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).NewListByGalleryApplicationPager), resourceGroupName, galleryName, galleryApplicationName, options) +} diff --git a/sources/azure/shared/mocks/mock_gallery_images_client.go b/sources/azure/shared/mocks/mock_gallery_images_client.go new file mode 100644 index 00000000..585f8825 --- /dev/null +++ b/sources/azure/shared/mocks/mock_gallery_images_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: gallery-images-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_gallery_images_client.go -package=mocks -source=gallery-images-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleryImagesClient is a mock of GalleryImagesClient interface. +type MockGalleryImagesClient struct { + ctrl *gomock.Controller + recorder *MockGalleryImagesClientMockRecorder + isgomock struct{} +} + +// MockGalleryImagesClientMockRecorder is the mock recorder for MockGalleryImagesClient. +type MockGalleryImagesClientMockRecorder struct { + mock *MockGalleryImagesClient +} + +// NewMockGalleryImagesClient creates a new mock instance. +func NewMockGalleryImagesClient(ctrl *gomock.Controller) *MockGalleryImagesClient { + mock := &MockGalleryImagesClient{ctrl: ctrl} + mock.recorder = &MockGalleryImagesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryImagesClient) EXPECT() *MockGalleryImagesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryImagesClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryImageName string, options *armcompute.GalleryImagesClientGetOptions) (armcompute.GalleryImagesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryImageName, options) + ret0, _ := ret[0].(armcompute.GalleryImagesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryImagesClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryImageName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryImagesClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryImageName, options) +} + +// NewListByGalleryPager mocks base method. +func (m *MockGalleryImagesClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) clients.GalleryImagesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryPager", resourceGroupName, galleryName, options) + ret0, _ := ret[0].(clients.GalleryImagesPager) + return ret0 +} + +// NewListByGalleryPager indicates an expected call of NewListByGalleryPager. +func (mr *MockGalleryImagesClientMockRecorder) NewListByGalleryPager(resourceGroupName, galleryName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryPager", reflect.TypeOf((*MockGalleryImagesClient)(nil).NewListByGalleryPager), resourceGroupName, galleryName, options) +} diff --git a/sources/azure/shared/mocks/mock_shared_gallery_images_client.go b/sources/azure/shared/mocks/mock_shared_gallery_images_client.go new file mode 100644 index 00000000..5cff0b31 --- /dev/null +++ b/sources/azure/shared/mocks/mock_shared_gallery_images_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: shared-gallery-images-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_shared_gallery_images_client.go -package=mocks -source=shared-gallery-images-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSharedGalleryImagesClient is a mock of SharedGalleryImagesClient interface. +type MockSharedGalleryImagesClient struct { + ctrl *gomock.Controller + recorder *MockSharedGalleryImagesClientMockRecorder + isgomock struct{} +} + +// MockSharedGalleryImagesClientMockRecorder is the mock recorder for MockSharedGalleryImagesClient. +type MockSharedGalleryImagesClientMockRecorder struct { + mock *MockSharedGalleryImagesClient +} + +// NewMockSharedGalleryImagesClient creates a new mock instance. +func NewMockSharedGalleryImagesClient(ctrl *gomock.Controller) *MockSharedGalleryImagesClient { + mock := &MockSharedGalleryImagesClient{ctrl: ctrl} + mock.recorder = &MockSharedGalleryImagesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSharedGalleryImagesClient) EXPECT() *MockSharedGalleryImagesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSharedGalleryImagesClient) Get(ctx context.Context, location, galleryUniqueName, galleryImageName string, options *armcompute.SharedGalleryImagesClientGetOptions) (armcompute.SharedGalleryImagesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, location, galleryUniqueName, galleryImageName, options) + ret0, _ := ret[0].(armcompute.SharedGalleryImagesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSharedGalleryImagesClientMockRecorder) Get(ctx, location, galleryUniqueName, galleryImageName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSharedGalleryImagesClient)(nil).Get), ctx, location, galleryUniqueName, galleryImageName, options) +} + +// NewListPager mocks base method. +func (m *MockSharedGalleryImagesClient) NewListPager(location, galleryUniqueName string, options *armcompute.SharedGalleryImagesClientListOptions) clients.SharedGalleryImagesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", location, galleryUniqueName, options) + ret0, _ := ret[0].(clients.SharedGalleryImagesPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockSharedGalleryImagesClientMockRecorder) NewListPager(location, galleryUniqueName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockSharedGalleryImagesClient)(nil).NewListPager), location, galleryUniqueName, options) +} diff --git a/sources/azure/shared/mocks/mock_snapshots_client.go b/sources/azure/shared/mocks/mock_snapshots_client.go new file mode 100644 index 00000000..bd7a9159 --- /dev/null +++ b/sources/azure/shared/mocks/mock_snapshots_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: snapshots-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_snapshots_client.go -package=mocks -source=snapshots-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSnapshotsClient is a mock of SnapshotsClient interface. +type MockSnapshotsClient struct { + ctrl *gomock.Controller + recorder *MockSnapshotsClientMockRecorder + isgomock struct{} +} + +// MockSnapshotsClientMockRecorder is the mock recorder for MockSnapshotsClient. +type MockSnapshotsClientMockRecorder struct { + mock *MockSnapshotsClient +} + +// NewMockSnapshotsClient creates a new mock instance. +func NewMockSnapshotsClient(ctrl *gomock.Controller) *MockSnapshotsClient { + mock := &MockSnapshotsClient{ctrl: ctrl} + mock.recorder = &MockSnapshotsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSnapshotsClient) EXPECT() *MockSnapshotsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSnapshotsClient) Get(ctx context.Context, resourceGroupName, snapshotName string, options *armcompute.SnapshotsClientGetOptions) (armcompute.SnapshotsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, snapshotName, options) + ret0, _ := ret[0].(armcompute.SnapshotsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSnapshotsClientMockRecorder) Get(ctx, resourceGroupName, snapshotName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSnapshotsClient)(nil).Get), ctx, resourceGroupName, snapshotName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockSnapshotsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.SnapshotsClientListByResourceGroupOptions) clients.SnapshotsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.SnapshotsPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockSnapshotsClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockSnapshotsClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index 572659ac..0025c3c7 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -46,28 +46,38 @@ const ( // Maintenance Maintenance shared.API = "maintenance" // Microsoft.Maintenance + + // Resources (subscriptions, resource groups) + Resources shared.API = "resources" // Microsoft.Resources ) // Resources // These represent the actual resource types within each Azure resource provider const ( // Compute resources - VirtualMachine shared.Resource = "virtual-machine" - Disk shared.Resource = "disk" - AvailabilitySet shared.Resource = "availability-set" - VirtualMachineExtension shared.Resource = "virtual-machine-extension" - VirtualMachineRunCommand shared.Resource = "virtual-machine-run-command" - VirtualMachineScaleSet shared.Resource = "virtual-machine-scale-set" - DiskEncryptionSet shared.Resource = "disk-encryption-set" - ProximityPlacementGroup shared.Resource = "proximity-placement-group" - DedicatedHostGroup shared.Resource = "dedicated-host-group" - CapacityReservationGroup shared.Resource = "capacity-reservation-group" - Image shared.Resource = "image" - Snapshot shared.Resource = "snapshot" - DiskAccess shared.Resource = "disk-access" - SharedGalleryImage shared.Resource = "shared-gallery-image" - CommunityGalleryImage shared.Resource = "community-gallery-image" - SharedGalleryApplicationVersion shared.Resource = "shared-gallery-application-version" + VirtualMachine shared.Resource = "virtual-machine" + Disk shared.Resource = "disk" + AvailabilitySet shared.Resource = "availability-set" + VirtualMachineExtension shared.Resource = "virtual-machine-extension" + VirtualMachineRunCommand shared.Resource = "virtual-machine-run-command" + VirtualMachineScaleSet shared.Resource = "virtual-machine-scale-set" + DiskEncryptionSet shared.Resource = "disk-encryption-set" + ProximityPlacementGroup shared.Resource = "proximity-placement-group" + DedicatedHostGroup shared.Resource = "dedicated-host-group" + DedicatedHost shared.Resource = "dedicated-host" + CapacityReservationGroup shared.Resource = "capacity-reservation-group" + CapacityReservation shared.Resource = "capacity-reservation" + Image shared.Resource = "image" + Snapshot shared.Resource = "snapshot" + DiskAccess shared.Resource = "disk-access" + DiskAccessPrivateEndpointConnection shared.Resource = "disk-access-private-endpoint-connection" + SharedGalleryImage shared.Resource = "shared-gallery-image" + SharedGallery shared.Resource = "shared-gallery" + CommunityGalleryImage shared.Resource = "community-gallery-image" + GalleryApplicationVersion shared.Resource = "gallery-application-version" + GalleryApplication shared.Resource = "gallery-application" + GalleryImage shared.Resource = "gallery-image" + Gallery shared.Resource = "gallery" // Network resources VirtualNetwork shared.Resource = "virtual-network" diff --git a/sources/azure/shared/scope.go b/sources/azure/shared/scope.go index 3c0a9281..bed1d144 100644 --- a/sources/azure/shared/scope.go +++ b/sources/azure/shared/scope.go @@ -3,7 +3,7 @@ package shared import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index adb4746e..941ea07f 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -22,12 +22,14 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-storage-blob-container": {"storageAccounts", "containers"}, "azure-storage-file-share": {"storageAccounts", "shares"}, "azure-storage-table": {"storageAccounts", "tables"}, - "azure-sql-database": {"servers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-SQL-SouthEastAsia/providers/Microsoft.Sql/servers/testsvr/databases/testdb", - "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-PostgreSQL-SouthEastAsia/providers/Microsoft.DBforPostgreSQL/flexibleServers/testsvr/databases/testdb", - "azure-keyvault-secret": {"vaults", "secrets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}", - "azure-authorization-role-assignment": {"roleAssignments"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName}", - "azure-compute-virtual-machine-run-command": {"virtualMachines", "runCommands"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/runCommands/{runCommandName}", - "azure-compute-virtual-machine-extension": {"virtualMachines", "extensions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/extensions/{extensionName}", + "azure-sql-database": {"servers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-SQL-SouthEastAsia/providers/Microsoft.Sql/servers/testsvr/databases/testdb", + "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-PostgreSQL-SouthEastAsia/providers/Microsoft.DBforPostgreSQL/flexibleServers/testsvr/databases/testdb", + "azure-keyvault-secret": {"vaults", "secrets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}", + "azure-authorization-role-assignment": {"roleAssignments"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName}", + "azure-compute-virtual-machine-run-command": {"virtualMachines", "runCommands"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/runCommands/{runCommandName}", + "azure-compute-virtual-machine-extension": {"virtualMachines", "extensions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/extensions/{extensionName}", + "azure-compute-gallery-application-version": {"galleries", "applications", "versions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{applicationName}/versions/{versionName}", + "azure-compute-gallery-image": {"galleries", "images"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}", } if keys, ok := pathKeysMap[resourceType]; ok { @@ -61,6 +63,10 @@ func ExtractResourceName(resourceID string) string { // For example, for input="/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/queueServices/default/queues/{queue}" // and keys=["storageAccounts", "queues"], it will return ["{account}", "{queue}"]. // +// Key matching is case-insensitive (Azure resource IDs are case-insensitive) but +// only matches at even-indexed path positions (structural key slots) to avoid +// misidentifying a resource name that happens to equal a key. +// // If a key is not found, the function will return nil. func ExtractPathParamsFromResourceID(resourceID string, keys []string) []string { if resourceID == "" || len(keys) == 0 { @@ -73,7 +79,11 @@ func ExtractPathParamsFromResourceID(resourceID string, keys []string) []string for _, key := range keys { found := false for i, part := range parts { - if part == key && i+1 < len(parts) { + // Azure resource IDs alternate key/value segments after trimming: + // key0/value0/key1/value1/... Keys are at even indices (0, 2, 4, ...), + // values at odd indices. Only match at key positions to prevent a + // resource name like "images" from being treated as a path key. + if i%2 == 0 && strings.EqualFold(part, key) && i+1 < len(parts) { results = append(results, parts[i+1]) found = true break @@ -406,20 +416,25 @@ func ExtractDNSFromURL(urlStr string) string { return urlStr } -// ExtractStorageAccountNameFromBlobURI extracts the storage account name from an Azure blob URI -// Blob URIs follow the format: https://{accountName}.blob.core.windows.net/{container}/{blob} +// ExtractStorageAccountNameFromBlobURI extracts the storage account name from an Azure blob URI. +// Blob URIs use the host format {accountName}.blob.core.{suffix} in all Azure clouds, e.g.: +// - Public: https://{accountName}.blob.core.windows.net/{container}/{blob} +// - China: https://{accountName}.blob.core.chinacloudapi.cn/... +// - US Government: https://{accountName}.blob.core.usgovcloudapi.net/... func ExtractStorageAccountNameFromBlobURI(blobURI string) string { if blobURI == "" { return "" } - parsedURL, err := url.Parse(blobURI) if err != nil { return "" } - host := parsedURL.Host - // Extract account name from hostname: {accountName}.blob.core.windows.net + // Accept any Azure blob endpoint (public and sovereign clouds); check host only to avoid matching path/query + if !strings.Contains(host, ".blob.core.") { + return "" + } + // Account name is the first label of the host in all Azure blob endpoints parts := strings.Split(host, ".") if len(parts) > 0 && parts[0] != "" { return parts[0] @@ -428,24 +443,21 @@ func ExtractStorageAccountNameFromBlobURI(blobURI string) string { return "" } -// ExtractContainerNameFromBlobURI extracts the container name from an Azure blob URI -// Blob URIs follow the format: https://{accountName}.blob.core.windows.net/{container}/{blob} -// Returns the first path segment which is the container name +// ExtractContainerNameFromBlobURI extracts the container name from an Azure blob URI. +// Blob URIs use the same path layout in all Azure clouds; the first path segment is the container. +// Returns the first path segment which is the container name. func ExtractContainerNameFromBlobURI(blobURI string) string { if blobURI == "" { return "" } - - // Defensive check: ensure this is actually a blob URI - if !strings.Contains(blobURI, ".blob.core.windows.net") { - return "" - } - parsedURL, err := url.Parse(blobURI) if err != nil { return "" } - + // Ensure this is an Azure blob host (public or sovereign cloud); check host only to avoid matching path/query + if !strings.Contains(parsedURL.Host, ".blob.core.") { + return "" + } path := strings.Trim(parsedURL.Path, "/") if path == "" { return "" diff --git a/sources/azure/shared/utils_test.go b/sources/azure/shared/utils_test.go index e90dc62e..21126095 100644 --- a/sources/azure/shared/utils_test.go +++ b/sources/azure/shared/utils_test.go @@ -205,6 +205,36 @@ func TestExtractPathParamsFromResourceID(t *testing.T) { keys: []string{"storageAccounts", "queues"}, expected: []string{"test-storage-account_123", "test_queue-name"}, }, + { + name: "case-insensitive key matching - lowercase keys match uppercase segments", + resourceID: "/CommunityGalleries/test-gallery/Images/test-image/Versions/1.0.0", + keys: []string{"communitygalleries", "images", "versions"}, + expected: []string{"test-gallery", "test-image", "1.0.0"}, + }, + { + name: "case-insensitive key matching - uppercase keys match lowercase segments", + resourceID: "/communitygalleries/test-gallery/images/test-image/versions/1.0.0", + keys: []string{"CommunityGalleries", "Images", "Versions"}, + expected: []string{"test-gallery", "test-image", "1.0.0"}, + }, + { + name: "case-insensitive key matching - mixed case", + resourceID: "/subscriptions/12345678/resourcegroups/test-rg/providers/Microsoft.Storage/storageaccounts/myaccount", + keys: []string{"storageAccounts"}, + expected: []string{"myaccount"}, + }, + { + name: "value matching key name is not misidentified - gallery named 'images'", + resourceID: "/galleries/images/images/real-image/versions/1.0.0", + keys: []string{"images", "versions"}, + expected: []string{"real-image", "1.0.0"}, + }, + { + name: "value matching key name is not misidentified - gallery named 'versions'", + resourceID: "/galleries/versions/images/real-image/versions/1.0.0", + keys: []string{"images", "versions"}, + expected: []string{"real-image", "1.0.0"}, + }, } for _, tc := range tests { @@ -971,3 +1001,56 @@ func TestExtractDNSFromURL(t *testing.T) { }) } } + +func TestExtractStorageAccountNameFromBlobURI(t *testing.T) { + tests := []struct { + name string + blobURI string + expected string + }{ + { + name: "valid blob URI", + blobURI: "https://mystorageaccount.blob.core.windows.net/container/blob", + expected: "mystorageaccount", + }, + { + name: "blob URI with path only", + blobURI: "https://account.blob.core.windows.net/packages/app.zip", + expected: "account", + }, + { + name: "sovereign cloud China blob URI", + blobURI: "https://myaccount.blob.core.chinacloudapi.cn/container/blob", + expected: "myaccount", + }, + { + name: "sovereign cloud US Government blob URI", + blobURI: "https://myaccount.blob.core.usgovcloudapi.net/container/blob", + expected: "myaccount", + }, + { + name: "non-blob HTTPS URL must return empty", + blobURI: "https://example.com/artifacts/app.zip", + expected: "", + }, + { + name: "non-blob HTTP URL must return empty", + blobURI: "http://cdn.example.com/foo", + expected: "", + }, + { + name: "empty URI", + blobURI: "", + expected: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := azureshared.ExtractStorageAccountNameFromBlobURI(tc.blobURI) + if actual != tc.expected { + t.Errorf("ExtractStorageAccountNameFromBlobURI(%q) = %q; want %q", tc.blobURI, actual, tc.expected) + } + }) + } +} diff --git a/sources/example/base.go b/sources/example/base.go index 528c9702..2bbf09bd 100644 --- a/sources/example/base.go +++ b/sources/example/base.go @@ -3,7 +3,7 @@ package example import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/example/custom_searchable_listable.go b/sources/example/custom_searchable_listable.go index d7efcd54..94ad2997 100644 --- a/sources/example/custom_searchable_listable.go +++ b/sources/example/custom_searchable_listable.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -142,10 +142,6 @@ func (d *customComputeInstanceWrapper) externalTypeToSDPItem(external *ExternalT Query: external.LinkedItemID, Scope: d.Scopes()[0], }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } diff --git a/sources/example/errors.go b/sources/example/errors.go index 84cd4d8d..a83b7bc8 100644 --- a/sources/example/errors.go +++ b/sources/example/errors.go @@ -3,7 +3,7 @@ package example import ( "errors" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // queryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/example/metadata_test.go b/sources/example/metadata_test.go index a4ecaec8..db60d638 100644 --- a/sources/example/metadata_test.go +++ b/sources/example/metadata_test.go @@ -8,8 +8,8 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/example/shared" ) diff --git a/sources/example/standard_searchable_listable.go b/sources/example/standard_searchable_listable.go index 755515c0..1d6097f5 100644 --- a/sources/example/standard_searchable_listable.go +++ b/sources/example/standard_searchable_listable.go @@ -3,7 +3,7 @@ package example import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" exampleshared "github.com/overmindtech/cli/sources/example/shared" "github.com/overmindtech/cli/sources/shared" @@ -163,10 +163,6 @@ func (d *computeInstanceWrapper) externalTypeToSDPItem(external *ExternalType) ( Query: external.LinkedItemID, Scope: d.Scopes()[0], }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } diff --git a/sources/example/standard_searchable_listable_test.go b/sources/example/standard_searchable_listable_test.go index d20fc984..44c0c1a2 100644 --- a/sources/example/standard_searchable_listable_test.go +++ b/sources/example/standard_searchable_listable_test.go @@ -6,7 +6,7 @@ import ( "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/example" "github.com/overmindtech/cli/sources/example/mocks" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/example/validation_test.go b/sources/example/validation_test.go index 28dc926e..88ddcb42 100644 --- a/sources/example/validation_test.go +++ b/sources/example/validation_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" ) diff --git a/sources/gcp/README.md b/sources/gcp/README.md index e60a635e..30e4e642 100644 --- a/sources/gcp/README.md +++ b/sources/gcp/README.md @@ -116,15 +116,12 @@ When defining a relation between two adapters, we need to answer the following q - What is the method to use to get the related item?: `sdp.QueryMethod_GET`, `sdp.QueryMethod_SEARCH`, `sdp.QueryMethod_LIST` - What is the query string to pass to the selected method? - What is the scope of the related item?: `project`, `region`, `zone` -- How is the relation between the two items?: `BlastPropagation` - In the following example, we define a relation between the `ComputeInstance` and `ComputeSubnetwork` adapters. - We identify the `ComputeSubnetwork` adapter as the related item. - We use the `sdp.QueryMethod_GET` method to get the related item. Because the attribute `subnetwork_name` can be used to get the `ComputeSubnetwork` resource. If it was an attribute that can be used for searching, we would use the `sdp.QueryMethod_SEARCH` method. By the time we are developing the adapter, the linked adapter may not be present. In that case, we have to research the linked adapter and make the correct judgement. - We use the `subnetworkName` as the query string to pass to the `GET` method. Because its [SDK documentation](https://cloud.google.com/compute/docs/reference/rest/v1/subnetworks/get) states that we need to pass its `name` to get the resource. - We define the scope as `region` via the `gcpshared.RegionalScope(c.ProjectID(), region)` helper function. Because the `ComputeSubnetwork` resource is a regional resource. It requires the `project_id` and `region` along with its `name` to get the resource. -- We define the relation as `BlastPropagation` with `In: true` and `Out: false`. Because the adapter we define is the `ComputeInstance` adapter, we want to propagate the blast radius from the `ComputeInstance` to the `ComputeSubnetwork`. This means that if the `ComputeSubnetwork` is deleted, the `ComputeInstance` will be affected by that (`in:true`). But if the `ComputeInstance` is deleted, the `ComputeSubnetwork` will not be affected (`out:false`). The relation might not be that clear all the time. In this case we should err on to `true` side. ```go &sdp.LinkedItemQuery{ @@ -135,10 +132,6 @@ In the following example, we define a relation between the `ComputeInstance` and // This is a regional resource Scope: gcpshared.RegionalScope(c.ProjectID(), region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } ``` diff --git a/sources/gcp/build/package/Dockerfile b/sources/gcp/build/package/Dockerfile index 538d6b40..853a571a 100644 --- a/sources/gcp/build/package/Dockerfile +++ b/sources/gcp/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source sources/gcp/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source sources/gcp/main.go FROM alpine:3.23 WORKDIR / diff --git a/sources/gcp/cmd/root.go b/sources/gcp/cmd/root.go index 30cb4aae..08ef2d42 100644 --- a/sources/gcp/cmd/root.go +++ b/sources/gcp/cmd/root.go @@ -9,15 +9,15 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/logging" + "github.com/overmindtech/cli/go/logging" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/sources/gcp/proc" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" ) var cfgFile string diff --git a/sources/gcp/dynamic/adapter-listable.go b/sources/gcp/dynamic/adapter-listable.go index 5a4d9fda..02c745d0 100644 --- a/sources/gcp/dynamic/adapter-listable.go +++ b/sources/gcp/dynamic/adapter-listable.go @@ -6,10 +6,11 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -32,7 +33,7 @@ func NewListableAdapter(listEndpointFunc gcpshared.ListEndpointFunc, config *Ada sdpAdapterCategory: config.SDPAdapterCategory, terraformMappings: config.TerraformMappings, linker: config.Linker, - potentialLinks: potentialLinksFromBlasts(config.SDPAssetType, gcpshared.BlastPropagations), + potentialLinks: potentialLinksFromLinkRules(config.SDPAssetType, gcpshared.LinkRules), uniqueAttributeKeys: config.UniqueAttributeKeys, iamPermissions: config.IAMPermissions, nameSelector: config.NameSelector, @@ -75,13 +76,18 @@ func (g ListableAdapter) List(ctx context.Context, scope string, ignoreCache boo defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_LIST.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit { @@ -90,20 +96,35 @@ func (g ListableAdapter) List(ctx context.Context, scope string, ignoreCache boo listURL, err := g.listEndpointFunc(location) if err != nil { - err := &sdp.QueryError{ + return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: fmt.Sprintf("failed to construct list endpoint: %v", err), } - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) - return nil, err } items, err := aggregateSDPItems(ctx, g.Adapter, listURL, location) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + return []*sdp.Item{}, nil + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found in scope %s", g.Type(), scope), + Scope: scope, + SourceName: g.Name(), + ItemType: g.Type(), + ResponderName: g.Name(), + } + g.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + for _, item := range items { g.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) } @@ -130,13 +151,19 @@ func (g ListableAdapter) ListStream(ctx context.Context, scope string, ignoreCac defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_LIST.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { diff --git a/sources/gcp/dynamic/adapter-searchable-listable.go b/sources/gcp/dynamic/adapter-searchable-listable.go index 34d39f1f..d439690e 100644 --- a/sources/gcp/dynamic/adapter-searchable-listable.go +++ b/sources/gcp/dynamic/adapter-searchable-listable.go @@ -7,10 +7,11 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -42,7 +43,7 @@ func NewSearchableListableAdapter(searchURLFunc gcpshared.EndpointFunc, listEndp sdpAdapterCategory: config.SDPAdapterCategory, terraformMappings: config.TerraformMappings, linker: config.Linker, - potentialLinks: potentialLinksFromBlasts(config.SDPAssetType, gcpshared.BlastPropagations), + potentialLinks: potentialLinksFromLinkRules(config.SDPAssetType, gcpshared.LinkRules), uniqueAttributeKeys: config.UniqueAttributeKeys, iamPermissions: config.IAMPermissions, nameSelector: config.NameSelector, @@ -88,13 +89,18 @@ func (g SearchableListableAdapter) Search(ctx context.Context, scope, query stri defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit { @@ -110,20 +116,35 @@ func (g SearchableListableAdapter) Search(ctx context.Context, scope, query stri searchEndpoint := g.searchEndpointFunc(query, location) if searchEndpoint == "" { - err := &sdp.QueryError{ + return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: fmt.Sprintf("no search endpoint found for query \"%s\". %s", query, g.Metadata().GetSupportedQueryMethods().GetSearchDescription()), } - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) - return nil, err } items, err := aggregateSDPItems(ctx, g.Adapter, searchEndpoint, location) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + return []*sdp.Item{}, nil + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found for search query '%s'", g.Type(), query), + Scope: scope, + SourceName: g.Name(), + ItemType: g.Type(), + ResponderName: g.Name(), + } + g.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + for _, item := range items { g.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) } @@ -150,13 +171,19 @@ func (g SearchableListableAdapter) SearchStream(ctx context.Context, scope, quer defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { @@ -179,6 +206,10 @@ func (g SearchableListableAdapter) SearchStream(ctx context.Context, scope, quer }) return } + if len(items) == 0 { + // NOTFOUND: terraformMappingViaSearch returns ([], nil); send nothing (matches cached NOTFOUND behaviour) + return + } g.cache.StoreItem(ctx, items[0], shared.DefaultCacheDuration, ck) // There should only be one item in the result, so we can send it directly diff --git a/sources/gcp/dynamic/adapter-searchable.go b/sources/gcp/dynamic/adapter-searchable.go index b3f009eb..ebebfb53 100644 --- a/sources/gcp/dynamic/adapter-searchable.go +++ b/sources/gcp/dynamic/adapter-searchable.go @@ -7,10 +7,11 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -36,7 +37,7 @@ func NewSearchableAdapter(searchEndpointFunc gcpshared.EndpointFunc, config *Ada sdpAdapterCategory: config.SDPAdapterCategory, terraformMappings: config.TerraformMappings, linker: config.Linker, - potentialLinks: potentialLinksFromBlasts(config.SDPAssetType, gcpshared.BlastPropagations), + potentialLinks: potentialLinksFromLinkRules(config.SDPAssetType, gcpshared.LinkRules), uniqueAttributeKeys: config.UniqueAttributeKeys, iamPermissions: config.IAMPermissions, nameSelector: config.NameSelector, @@ -78,13 +79,18 @@ func (g SearchableAdapter) Search(ctx context.Context, scope, query string, igno ) defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit { @@ -101,20 +107,35 @@ func (g SearchableAdapter) Search(ctx context.Context, scope, query string, igno // This is a regular SEARCH call searchEndpoint := g.searchEndpointFunc(query, location) if searchEndpoint == "" { - err := &sdp.QueryError{ + return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: fmt.Sprintf("no search endpoint found for query \"%s\". %s", query, g.Metadata().GetSupportedQueryMethods().GetSearchDescription()), } - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) - return nil, err } items, err := aggregateSDPItems(ctx, g.Adapter, searchEndpoint, location) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + return []*sdp.Item{}, nil + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found for search query '%s'", g.Type(), query), + Scope: scope, + SourceName: g.Name(), + ItemType: g.Type(), + ResponderName: g.Name(), + } + g.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + for _, item := range items { g.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) } @@ -140,13 +161,19 @@ func (g SearchableAdapter) SearchStream(ctx context.Context, scope, query string ) defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { @@ -169,6 +196,10 @@ func (g SearchableAdapter) SearchStream(ctx context.Context, scope, query string }) return } + if len(items) == 0 { + // NOTFOUND: terraformMappingViaSearch returns ([], nil); send nothing (matches cached NOTFOUND behaviour) + return + } g.cache.StoreItem(ctx, items[0], shared.DefaultCacheDuration, ck) // There should only be one item in the result, so we can send it directly diff --git a/sources/gcp/dynamic/adapter.go b/sources/gcp/dynamic/adapter.go index f04c9674..f301d12a 100644 --- a/sources/gcp/dynamic/adapter.go +++ b/sources/gcp/dynamic/adapter.go @@ -8,10 +8,11 @@ import ( "buf.build/go/protovalidate" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -58,7 +59,7 @@ func NewAdapter(config *AdapterConfig, cache sdpcache.Cache) discovery.Adapter { sdpAdapterCategory: config.SDPAdapterCategory, terraformMappings: config.TerraformMappings, linker: config.Linker, - potentialLinks: potentialLinksFromBlasts(config.SDPAssetType, gcpshared.BlastPropagations), + potentialLinks: potentialLinksFromLinkRules(config.SDPAssetType, gcpshared.LinkRules), uniqueAttributeKeys: config.UniqueAttributeKeys, iamPermissions: config.IAMPermissions, nameSelector: config.NameSelector, @@ -130,13 +131,18 @@ func (g Adapter) Get(ctx context.Context, scope string, query string, ignoreCach ) defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into nil result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return nil, qErr + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_GET.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit && len(cachedItem) > 0 { @@ -153,19 +159,24 @@ func (g Adapter) Get(ctx context.Context, scope string, query string, ignoreCach g.Metadata().GetSupportedQueryMethods().GetGetDescription(), ), } - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) return nil, err } resp, err := externalCallSingle(ctx, g.httpCli, url) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + enrichNOTFOUNDQueryError(err, scope, g.Name(), g.Type()) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } item, err := externalToSDP(ctx, location, g.uniqueAttributeKeys, resp, g.sdpAssetType, g.linker, g.nameSelector) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + enrichNOTFOUNDQueryError(err, scope, g.Name(), g.Type()) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } diff --git a/sources/gcp/dynamic/adapters.go b/sources/gcp/dynamic/adapters.go index 82835a22..1011183a 100644 --- a/sources/gcp/dynamic/adapters.go +++ b/sources/gcp/dynamic/adapters.go @@ -4,8 +4,8 @@ import ( "fmt" "net/http" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc deleted file mode 100644 index e2aef159..00000000 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc +++ /dev/null @@ -1,490 +0,0 @@ ---- -description: "GCP Dynamic Adapter development patterns and standards" -globs: **/*.go -alwaysApply: false ---- - -# Dynamic Adapter Creation Rules - -## Overview - -When creating new GCP dynamic adapters in the Overmind codebase, follow these patterns and requirements to ensure consistency, correctness, and maintainability. - -## Core Principles - -- Always use the `registerableAdapter` struct pattern (NOT legacy `SDPAssetTypeToAdapterMeta`) -- Implement proper scope detection (project/regional/zonal) -- Include comprehensive blast propagation analysis -- Add proper terraform mapping support -- Follow naming conventions and IAM permissions -- Research from official API documentation - -## Implementation Steps - -### 1. Resource Analysis - -#### Understand the Resource -- **Add clear description**: What does this resource do? Add a descriptive comment at the top of the adapter -- **Research the API**: Study the official GCP API documentation thoroughly -- **Identify use cases**: How is this resource typically used in GCP infrastructure? - -#### Determine Scope -- **Project-level**: URLs with `/global/` or no location parameters - - Examples: `compute-global-forwarding-rule.go`, `monitoring-alert-policy.go` -- **Regional**: URLs with `/regions/{region}/` - - Examples: `dataproc-cluster.go`, `run-service.go` -- **Zonal**: URLs with `/zones/{zone}/` - - Examples: Check existing zonal adapters in the directory - -#### Identify Unique Attributes -- Usually the resource name in the URL path -- Extract from API URL path segments -- Consider multiple unique attributes for complex resources - -### 2. Adapter Structure - -Use the `registerableAdapter` struct pattern with these key components: - -```go -var YourResourceAdapter = ®isterableAdapter{ - SDPItemType: gcpshared.YourItemType, - Category: sdp.AdapterCategory_ADAPTER_CATEGORY_YOUR_CATEGORY, - Scope: gcpshared.ScopeYourScope, - UniqueAttributes: []string{"yourUniqueAttribute"}, - IAMPermissions: []string{"api.resource.get", "api.resource.list"}, - TerraformResource: "google_your_resource", - // ... other fields -} -``` - -#### Key Components to Determine: - -- **SDP Item Type**: Define in `gcpshared` package (follow existing naming patterns) -- **Category**: Choose appropriate `sdp.AdapterCategory` - see `sdp-go/account.pb.go` for accurate category definitions and descriptions -- **Scope**: `gcpshared.ScopeProject`, `gcpshared.ScopeRegional`, or `gcpshared.ScopeZonal` -- **Unique Attributes**: Extract from API URL path segments -- **IAM Permissions**: Research from official API documentation -- **Terraform Resource**: Find correct resource name in Terraform registry - -### 3. Endpoint Functions - -Choose the appropriate endpoint function based on scope: - -#### Project-level Resources - -**Single Query Parameter:** -```go -GetFunc: ProjectLevelEndpointFuncWithSingleQuery( - "https://api.googleapis.com/v1/projects/{project}/resources/{resource}", - "resource", -), -ListFunc: ProjectLevelListFunc( - "https://api.googleapis.com/v1/projects/{project}/resources", -), -``` - -**Two Query Parameters (location + resource):** -```go -GetFunc: ProjectLevelEndpointFuncWithTwoQueries( - "https://api.googleapis.com/v1/projects/{project}/locations/{location}/resources/{resource}", -), -SearchFunc: ProjectLevelEndpointFuncWithSingleQuery( - "https://api.googleapis.com/v1/projects/{project}/locations/{location}/resources", -), -UniqueAttributeKeys: []string{"locations", "resources"}, -``` -*Examples: `cloudfunctions-function.go`, `run-service.go`, `file-instance.go`, `dataplex-data-scan.go`* - -#### Regional Resources - -**Single Query Parameter:** -```go -GetFunc: RegionalLevelEndpointFuncWithSingleQuery( - "https://api.googleapis.com/v1/projects/{project}/regions/{region}/resources/{resource}", - "resource", -), -ListFunc: RegionLevelListFunc( - "https://api.googleapis.com/v1/projects/{project}/regions/{region}/resources", -), -``` - -**Multiple Query Parameters:** -```go -GetFunc: RegionalLevelEndpointFuncWithTwoQueries( - "https://api.googleapis.com/v1/projects/{project}/regions/{region}/subresources/{subresource}/resources/{resource}", -), -UniqueAttributeKeys: []string{"subresources", "resources"}, -``` - -#### Zonal Resources - -**Single Query Parameter:** -```go -GetFunc: ZoneLevelEndpointFuncWithSingleQuery( - "https://api.googleapis.com/v1/projects/{project}/zones/{zone}/resources/{resource}", - "resource", -), -ListFunc: ZoneLevelListFunc( - "https://api.googleapis.com/v1/projects/{project}/zones/{zone}/resources", -), -``` - -**Multiple Query Parameters:** -```go -GetFunc: ZoneLevelEndpointFuncWithTwoQueries( - "https://api.googleapis.com/v1/projects/{project}/zones/{zone}/subresources/{subresource}/resources/{resource}", -), -UniqueAttributeKeys: []string{"subresources", "resources"}, -``` - -### 4. List vs Search Decision - -#### Use List if: -- No additional parameters needed to list all resources in scope -- Simple resource listing without complex filtering - -#### Use Search if: -- Additional parameters (like location) required -- Multiple unique attributes -- Complex filtering needed - -#### No-op Search if: -- Terraform mapping needs SEARCH but adapter doesn't support it -- See `orgpolicy-policy.go` for example - -### 5. Blast Propagation Analysis - -For each attribute that references another resource: - -#### Ask the Right Questions: -- **IN**: "What happens if the linked resource is deleted/updated?" -- **OUT**: "What happens to the linked resource if this resource is updated/deleted?" - -#### Implementation: -```go -blastPropagation: map[string]*gcpshared.Impact{ - "network": { - In: true, // This resource is affected if network changes - Out: false, // Network is not affected if this resource changes - }, - "subnet": { - In: true, // This resource is affected if subnet changes - Out: true, // Subnet is affected if this resource changes - }, -}, -``` - -#### Special Cases: -- **Use dot notation**: `"config.networkUri"` for nested attributes -- **If no adapter exists**: Create the SDP item type definition in `sources/gcp/shared/item-types.go` and `models.go` as if the adapter exists, then link to it -- **Follow naming**: `gcp--` for new adapter types - -#### Creating Backlinks from Child to Parent Resources - -When a resource name contains hierarchical information (e.g., a database name includes its parent instance), you can create a backlink using the `name` field. The framework will automatically extract the parent resource identifier and create the appropriate linked item query. - -**Use Case**: Child resources that reference their parent in their name structure. - -**Example: Spanner Database to Instance Backlink** - -A Spanner database name has the format: `projects/{project}/instances/{instance}/databases/{database}` - -To create a backlink from the database to its parent instance: - -```go -// In sources/gcp/shared/blast-propagations.go -SpannerDatabase: { - // ... other blast propagations ... - - // This is a backlink to instance. - // Framework will extract the instance name and create the linked item query with GET - "name": { - Description: "If the Spanner Instance is deleted or updated: The Database may become invalid or inaccessible. If the Database is updated: The instance remains unaffected.", - ToSDPItemType: SpannerInstance, - BlastPropagation: impactInOnly, - }, -} -``` - -**How It Works:** -1. The framework reads the `name` field value (e.g., `projects/my-project/instances/test-instance/databases/my-db`) -2. It extracts the parent instance identifier (`test-instance`) -3. It creates a GET query for the parent SpannerInstance -4. The linked item query will have: - - Type: `gcp-spanner-instance` - - Method: GET - - Query: `test-instance` (extracted from database name) - - Scope: Same project as the database - -**Testing the Backlink:** - -When adding backlink blast propagations, verify they work correctly in tests: - -```go -// In adapter_test.go -{ - // name field creates a backlink to the Spanner instance - ExpectedType: gcpshared.SpannerInstance.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: instanceName, - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, -} -``` - -**When to Use This Pattern:** -- Child resources with hierarchical naming (database → instance, subnet → network) -- Parent resource is part of the child's name/path -- Relationship is one-way (child depends on parent, but parent doesn't depend on specific child) -- Use `impactInOnly` blast propagation (parent changes affect child, but not vice versa) - -#### Creating Forward Links from Parent to Child Resources - -When a parent resource needs to link to all its child resources, you can use the `IsParentToChild` flag to create a forward link using SEARCH. This is the inverse pattern of the backlink described above. - -**Use Case**: Parent resources that need to discover and link to all their child resources (e.g., instance → all databases). - -**Example: Spanner Instance to Databases Forward Link** - -A Spanner instance needs to link to all databases that belong to it. Since the instance doesn't have the database names, we use SEARCH to find all databases for that instance. - -To create a forward link from the instance to its databases: - -```go -// In the adapter file (e.g., spanner-instance.go) -var spannerInstanceAdapter = registerableAdapter{ - // ... other fields ... - - blastPropagation: map[string]*gcpshared.Impact{ - // This a link from parent to child via SEARCH - // We need to make sure that the linked item supports `SEARCH` method for the `instance` name. - "name": { - ToSDPItemType: gcpshared.SpannerDatabase, - Description: "If the Spanner Instance is deleted or updated: All associated databases may become invalid or inaccessible. If a database is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - IsParentToChild: true, - }, - }, -} -``` - -**How It Works:** -1. The framework detects `IsParentToChild: true` on the blast propagation -2. It modifies the unique attribute keys by removing the last element (the child identifier) - - For databases: `["instances", "databases"]` becomes `["instances"]` -3. It extracts only the parent identifier from the resource name (e.g., `test-instance`) -4. It creates a SEARCH query (not GET) to find all child resources -5. The linked item query will have: - - Type: `gcp-spanner-database` - - Method: SEARCH - - Query: `test-instance` (the instance name) - - Scope: Same project as the instance - -**Testing the Forward Link:** - -When adding parent-to-child blast propagations, verify they work correctly in tests: - -```go -// In adapter_test.go (e.g., spanner-instance_test.go) -{ - ExpectedType: gcpshared.SpannerDatabase.String(), - ExpectedMethod: sdp.QueryMethod_SEARCH, - ExpectedQuery: instanceName, - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, -} -``` - -**Critical Requirements:** -- **Child adapter must support SEARCH**: The child resource type must implement the SEARCH method -- **Query parameter must match**: The child's SEARCH method must accept the parent identifier as a search parameter -- **Set `IsParentToChild: true`**: This flag triggers the special SEARCH behavior - -**When to Use This Pattern:** -- Parent resources that need to discover all their children (instance → databases, network → subnets) -- The parent's name/identifier is sufficient to search for children -- The child adapter supports SEARCH method - -**Comparison: Backlink vs Forward Link** - -| Aspect | Backlink (Child → Parent) | Forward Link (Parent → Child) | -|--------|---------------------------|------------------------------| -| Direction | Child points to parent | Parent discovers children | -| Method | GET | SEARCH | -| Flag | No special flag | `IsParentToChild: true` | -| Use Case | Database → Instance | Instance → All Databases | -| Query | Single parent identifier | Parent identifier searches all children | - -### 6. Special Considerations - -#### NameSelector -- Use if resource doesn't have name attribute -- See `dataproc-cluster.go` for example - -#### Health Status -- Add TODO for status/state fields: `"TODO: https://linear.app/overmind/issue/ENG-631"` - -#### InDevelopment -- **DO NOT USE**: This flag is reserved for human authors only -- Agents should never set `InDevelopment: true` - -#### IAM Permissions -- Research from official API documentation -- Most APIs provide required permissions in their reference docs -- Common patterns: - - `"api.resource.get"` for individual resource access - - `"api.resource.list"` for listing resources - -#### Custom Role for Additional Permissions -When creating adapters that require permissions not available in GCP predefined roles with limited read-only access, you may need to update the Overmind custom role. - -**Current Custom Role Permissions:** -```hcl -resource "google_project_iam_custom_role" "overmind_custom_role" { - role_id = "overmindCustomRole" - title = "Overmind Custom Role" - description = "Custom role for Overmind service account with additional BigQuery and Spanner permissions" - permissions = [ - "bigquery.transfers.get", # BigQuery transfer configuration discovery - "spanner.databases.get", # Spanner database detail discovery - "spanner.databases.list", # Spanner database enumeration - ] -} -``` - -**When to Update the Custom Role:** -- If you cannot find a suitable predefined role that provides read-only access to your resource -- If the existing predefined roles have overly broad permissions (e.g., include data access) -- If you need specific permissions that are not included in any predefined role - -**Process for Adding New Permissions:** -1. Research the exact IAM permissions needed for your adapter from GCP API documentation -2. Verify that no predefined role exists with the required permissions -3. Add the new permissions to the custom role in these locations: - - `deploy/sources.tf` (infrastructure configuration) - - `docs.overmind.tech/docs/sources/gcp/configuration.md` (documentation) - - `frontend/src/features/settings/sources/details/gcp-scripts.ts` (customer scripts) -4. Update the roles reference table in documentation -5. Test the permissions work correctly with your adapter - -**Note:** The custom role is assigned separately from predefined roles to avoid Terraform `for_each` issues with apply-time computed values. Each service account gets both predefined roles (via `for_each`) and the custom role (via individual resource blocks). - -### 7. Terraform Mapping - -#### Research Requirements: -- Find correct resource name in Terraform registry: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/ -- Determine if method should be GET or SEARCH -- Match terraform mapping method to adapter capabilities - -#### Example: -```go -TerraformResource: "google_compute_global_forwarding_rule", -TerraformMethod: sdp.QueryMethod_GET, -``` - -### 8. API Service Enablement - -#### Ensure Required APIs Are Enabled: -- **CRITICAL**: All GCP APIs used by the adapter must be enabled in the deployment configuration -- Check if the API service is already included in `deploy/modules/ovm-services/gke.tf` -- If the API is missing, add it to the `google_project_service` resource - -#### Process: -1. **Identify API Service**: Extract the API service name from the adapter's endpoint URLs - - Example: `https://bigquerydatatransfer.googleapis.com/v1/...` → `bigquerydatatransfer.googleapis.com` -2. **Check Current APIs**: Review the list in `deploy/modules/ovm-services/gke.tf` -3. **Add Missing APIs**: If not present, add the API to the `overmind_apis` resource -4. **Follow Naming Convention**: Use the full API endpoint URL format -5. **Add Descriptive Comments**: Include a clear comment explaining what the API is for - -#### Example: -```hcl -resource "google_project_service" "overmind_apis" { - for_each = toset([ - "bigquerydatatransfer.googleapis.com", # BigQuery Data Transfer API - "new-api.googleapis.com", # New API for your adapter - // ... other APIs - ]) -} -``` - -#### Verification: -- Ensure all IAM permissions used in the adapter have their corresponding APIs enabled -- Verify against the `PredefinedRoles` map in `sources/gcp/shared/predefined-roles.go` -- All APIs referenced in predefined roles should be enabled in the deployment - -## Reference Examples - -### Project-level Resources -- `compute-global-forwarding-rule.go` -- `monitoring-alert-policy.go` - -### Regional Resources -- `dataproc-cluster.go` -- `run-service.go` - -### Zonal Resources -- Check existing zonal adapters in the directory - -### Special Patterns -- **Multiple Unique Attributes**: Check adapters with complex URL structures -- **No-op Search**: `orgpolicy-policy.go` -- **NameSelector**: `dataproc-cluster.go` -- **Complex Blast Propagation**: `dataproc-cluster.go` - -## Files to Create/Modify - -### Required Files -- `sources/gcp/dynamic/adapters/{name}.go` (main adapter file) - -### Optional Files (if new SDP item type needed) -- `sources/gcp/shared/item-types.go` (add new item type) -- `sources/gcp/shared/models.go` (add new model) - -## Important Patterns - -### Always Use: -- `registerableAdapter` struct, never legacy maps -- Endpoint functions that match the exact API URL format -- Clear and specific blast propagation descriptions -- Terraform mapping method that matches adapter capabilities -- IAM permissions that match the actual API requirements - -### Never Use: -- Legacy `SDPAssetTypeToAdapterMeta` pattern -- `InDevelopment: true` (reserved for human authors) -- Assumptions about protobuf types or API structure - -## Validation Checklist - -- [ ] Adapter file compiles without errors -- [ ] Proper scope detection and endpoint functions -- [ ] Comprehensive blast propagation analysis -- [ ] Valid terraform mapping with correct method (GET/SEARCH) -- [ ] Appropriate IAM permissions defined -- [ ] Custom role updated if new permissions required (see Custom Role section) -- [ ] Clear resource description and comments -- [ ] Follows existing adapter patterns -- [ ] No legacy `SDPAssetTypeToAdapterMeta` usage -- [ ] API URLs match official GCP documentation exactly -- [ ] Blast propagation covers all linked resources -- [ ] Terraform resource name verified in registry -- [ ] **Required APIs enabled in deployment**: All APIs used by the adapter are included in `deploy/modules/ovm-services/gke.tf` - -## Key Resources - -- **GCP API Documentation**: Always verify against official docs -- **Terraform Registry**: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/ -- **Existing Adapters**: Study patterns in `sources/gcp/dynamic/adapters/` -- **SDP Types**: Check `sources/gcp/shared/item-types.go` for existing types -- **Deployment Configuration**: Check `deploy/modules/ovm-services/gke.tf` for enabled APIs -- **Predefined Roles**: Review `sources/gcp/shared/predefined-roles.go` for IAM permissions diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc deleted file mode 100644 index 02b324a3..00000000 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc +++ /dev/null @@ -1,642 +0,0 @@ ---- -description: "GCP Dynamic Adapter unit testing patterns and standards" -globs: **/*_test.go ---- - -# Dynamic Adapter Unit Testing Rules - -## Overview - -When writing unit tests for GCP dynamic adapters in the Overmind codebase, follow these patterns and requirements to ensure consistency and correctness. - -## Package and Imports - -- **Package**: Always use `package adapters_test` (never `package adapters` or `package main`) -- **Required Imports**: - - ```go - import ( - "context" - "fmt" - "net/http" - "testing" - - // IMPORTANT: Import the correct protobuf package that matches the adapter's API endpoint - // Examples: - // "cloud.google.com/go/compute/apiv1/computepb" // Compute API v1 - // "cloud.google.com/go/functions/apiv2/functionspb" // Cloud Functions v2 - // "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" // AI Platform v1 - // "cloud.google.com/go/bigtable/admin/apiv2/adminpb" // BigTable Admin v2 - "cloud.google.com/go/yourservice/apiv1/yourservicepb" // Replace with actual service - - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sources/gcp/dynamic" - gcpshared "github.com/overmindtech/cli/sources/gcp/shared" - "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sources/stdlib" - ) - ``` - -## Protobuf Types - -- **CRITICAL: Match API Version and Package**: Always use the correct protobuf package and API version that matches the adapter's endpoint - - **Step 1**: Check the adapter file (e.g., `cloudfunctions-function.go`) for the API endpoint URL - - **Step 2**: Extract the API service and version from the URL: - - `https://compute.googleapis.com/compute/v1/...` → `cloud.google.com/go/compute/apiv1/computepb` - - `https://cloudfunctions.googleapis.com/v2/...` → `cloud.google.com/go/functions/apiv2/functionspb` - - `https://aiplatform.googleapis.com/v1/...` → `cloud.google.com/go/aiplatform/apiv1/aiplatformpb` - - `https://bigtableadmin.googleapis.com/v2/...` → `cloud.google.com/go/bigtable/admin/apiv2/adminpb` - - **Step 3**: Import the matching protobuf package and use its types (NOT generic `computepb`) - - **Example**: If adapter uses `https://cloudfunctions.googleapis.com/v2/...`, use `apiv2/functionspb`, NOT `apiv1` or `apiv2beta` -- **Verify Types**: Check available types with `go doc | grep -i "type.*YourResource"` - - Example: `go doc cloud.google.com/go/compute/apiv1/computepb | grep -i "type.*Address"` -- **Verify List Response Structure**: Check the actual list response type - field names vary by service - - Example: `computepb.AddressList` has `.Items` field - - Example: `aiplatformpb.ListModelDeploymentMonitoringJobsResponse` has `.ModelDeploymentMonitoringJobs` field -- **Common Mistakes**: - - Using `computepb` for non-Compute resources (use service-specific package) - - Assuming `GlobalAddress` exists (use `Address` instead) - - Not verifying the list response field name (it's not always `.Items`) -- **Always verify**: Don't assume protobuf types or field names exist - check the actual API package - -## Test Structure Template - -### Single Query Parameter Resources - -```go -func TestYourResource(t *testing.T) { - ctx := context.Background() - projectID := "test-project" - linker := gcpshared.NewLinker() - resourceName := "test-resource" - - // IMPORTANT: Use the correct protobuf package that matches your adapter's API endpoint - // Examples: - // - Compute API v1: computepb.Address - // - Functions API v2: functionspb.Function - // - AI Platform v1: aiplatformpb.Model - // Check the adapter file for the API endpoint URL and match the protobuf version! - - // Create mock protobuf object (replace YourServicepb with actual service package) - resource := &YourServicepb.YourResource{ - Name: &resourceName, - // ... other fields using pointer helpers - } - - // Create second resource for list testing - resourceName2 := "test-resource-2" - resource2 := &YourServicepb.YourResource{ - Name: &resourceName2, - // ... other fields using pointer helpers - } - - // Create list response with multiple items - // IMPORTANT: List response structure varies by service - verify the actual type and field names! - // Examples: - // - computepb.AddressList has .Items field - // - aiplatformpb.ListModelsResponse has .Models field - resourceList := &YourServicepb.YourResourceList{ - Items: []*YourServicepb.YourResource{resource, resource2}, // Field name may vary! - } - - sdpItemType := gcpshared.YourItemType - - // Mock HTTP responses - expectedCallAndResponses := map[string]shared.MockResponse{ - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/resources/%s", projectID, resourceName): { - StatusCode: http.StatusOK, - Body: resource, - }, - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/resources/%s", projectID, resourceName2): { - StatusCode: http.StatusOK, - Body: resource2, - }, - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/resources", projectID): { - StatusCode: http.StatusOK, - Body: resourceList, - }, - } - - t.Run("Get", func(t *testing.T) { - // Test Get functionality with StaticTests - }) - - t.Run("Search", func(t *testing.T) { - // Test Search functionality (for location-based resources) - // OR use List for project-level resources - }) - - t.Run("ErrorHandling", func(t *testing.T) { - // Test error responses (e.g., 404 Not Found) - }) -} -``` - -### Multiple Query Parameter Resources (e.g., location + resource) - -```go -func TestLocationBasedResource(t *testing.T) { - ctx := context.Background() - projectID := "test-project" - linker := gcpshared.NewLinker() - location := "us-central1" - resourceName := "test-resource" - - // IMPORTANT: Use the correct protobuf package that matches your adapter's API endpoint - // Check the adapter file for the API endpoint URL and match the service and version! - - // Create mock protobuf object (replace YourServicepb with actual service package) - resource := &YourServicepb.YourResource{ - Name: &resourceName, - // ... other fields using pointer helpers - } - - // Create second resource for list testing - resourceName2 := "test-resource-2" - resource2 := &YourServicepb.YourResource{ - Name: &resourceName2, - // ... other fields using pointer helpers - } - - // Create list response with multiple items - // IMPORTANT: List response structure varies - verify the actual type and field names! - resourceList := &YourServicepb.YourResourceList{ - Items: []*YourServicepb.YourResource{resource, resource2}, // Field name may vary! - } - - sdpItemType := gcpshared.YourItemType - - // Mock HTTP responses for location-based resources - expectedCallAndResponses := map[string]shared.MockResponse{ - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/locations/%s/resources/%s", projectID, location, resourceName): { - StatusCode: http.StatusOK, - Body: resource, - }, - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/locations/%s/resources/%s", projectID, location, resourceName2): { - StatusCode: http.StatusOK, - Body: resource2, - }, - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/locations/%s/resources", projectID, location): { - StatusCode: http.StatusOK, - Body: resourceList, - }, - } - - // Test Get with location + resource name - t.Run("Get", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - // For multiple query parameters, use the combined query format - combinedQuery := fmt.Sprintf("%s/%s", location, resourceName) - sdpItem, err := adapter.Get(ctx, projectID, combinedQuery, true) - if err != nil { - t.Fatalf("Failed to get resource: %v", err) - } - - // Validate SDP item properties - if sdpItem.GetType() != sdpItemType.String() { - t.Errorf("Expected type %s, got %s", sdpItemType.String(), sdpItem.GetType()) - } - if sdpItem.UniqueAttributeValue() != combinedQuery { - t.Errorf("Expected unique attribute value '%s', got %s", combinedQuery, sdpItem.UniqueAttributeValue()) - } - }) - - // Test Search (location-based resources typically use Search instead of List) - t.Run("Search", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - searchable, ok := adapter.(discovery.SearchableAdapter) - if !ok { - t.Skipf("Adapter for %s does not implement SearchableAdapter", sdpItemType) - } - - // Test location-based search - sdpItems, err := searchable.Search(ctx, projectID, location, true) - if err != nil { - t.Fatalf("Failed to search resources: %v", err) - } - - if len(sdpItems) != 2 { - t.Errorf("Expected 2 resources, got %d", len(sdpItems)) - } - }) - - t.Run("ErrorHandling", func(t *testing.T) { - // Test with error responses to simulate API errors - errorResponses := map[string]shared.MockResponse{ - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/locations/%s/resources/%s", projectID, location, resourceName): { - StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Resource not found"}, - }, - } - - httpCli := shared.NewMockHTTPClientProvider(errorResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - combinedQuery := shared.CompositeLookupKey(location, resourceName) - _, err = adapter.Get(ctx, projectID, combinedQuery, true) - if err == nil { - t.Error("Expected error when getting non-existent resource, but got nil") - } - }) -} -``` - -## Required Test Functions (Limit to These 3 Only) - -### 1. Get Test (MUST include StaticTests) - -```go -t.Run("Get", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - sdpItem, err := adapter.Get(ctx, projectID, resourceName, true) - if err != nil { - t.Fatalf("Failed to get resource: %v", err) - } - - // Validate SDP item properties - if sdpItem.GetType() != sdpItemType.String() { - t.Errorf("Expected type %s, got %s", sdpItemType.String(), sdpItem.GetType()) - } - if sdpItem.UniqueAttributeValue() != resourceName { - t.Errorf("Expected unique attribute value '%s', got %s", resourceName, sdpItem.UniqueAttributeValue()) - } - if sdpItem.GetScope() != projectID { - t.Errorf("Expected scope '%s', got %s", projectID, sdpItem.GetScope()) - } - - // Validate specific attributes - val, err := sdpItem.GetAttributes().Get("name") - if err != nil { - t.Fatalf("Failed to get 'name' attribute: %v", err) - } - if val != resourceName { - t.Errorf("Expected name field to be '%s', got %s", resourceName, val) - } - - // Include static tests - MUST cover ALL blast propagation links - t.Run("StaticTests", func(t *testing.T) { - // CRITICAL: Review the adapter's blast propagation configuration and to create - // test cases for EVERY linked resource defined in the adapter's blastPropagation map - // Check the adapter file (e.g., compute-global-address.go) for all blast propagation entries - // IMPORTANT: Always use Item types with .String() for ExpectedType (e.g., gcpshared.ComputeNetwork.String()) - // NEVER use pure strings (e.g., "gcp-compute-network") for ExpectedType - queryTests := shared.QueryTests{ - { - ExpectedType: gcpshared.LinkedResourceType.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "linked-resource-name", - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) -}) -``` - -### 2. List or Search Test (Choose based on adapter type) - -```go -// For location-based resources (use Search) -t.Run("Search", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - searchable, ok := adapter.(discovery.SearchableAdapter) - if !ok { - t.Skipf("Adapter for %s does not implement SearchableAdapter", sdpItemType) - } - - sdpItems, err := searchable.Search(ctx, projectID, location, true) - if err != nil { - t.Fatalf("Failed to search resources: %v", err) - } - - if len(sdpItems) != 2 { - t.Errorf("Expected 2 resources, got %d", len(sdpItems)) - } -}) - -// OR for project-level resources (use List) -t.Run("List", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - listable, ok := adapter.(discovery.ListableAdapter) - if !ok { - t.Skipf("Adapter for %s does not implement ListableAdapter", sdpItemType) - } - - sdpItems, err := listable.List(ctx, projectID, true) - if err != nil { - t.Fatalf("Failed to list resources: %v", err) - } - - if len(sdpItems) != 2 { - t.Errorf("Expected 2 resources, got %d", len(sdpItems)) - } -}) -``` - -### 3. ErrorHandling Test - -```go -t.Run("ErrorHandling", func(t *testing.T) { - // Test with error responses to simulate API errors - errorResponses := map[string]shared.MockResponse{ - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/resources/%s", projectID, resourceName): { - StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Resource not found"}, - }, - } - - httpCli := shared.NewMockHTTPClientProvider(errorResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - _, err = adapter.Get(ctx, projectID, resourceName, true) - if err == nil { - t.Error("Expected error when getting non-existent resource, but got nil") - } -}) -``` - -### 4. Search with Terraform Format Test (if adapter has terraform mappings with Search method) - -```go -t.Run("Search with Terraform format", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - searchable, ok := adapter.(discovery.SearchableAdapter) - if !ok { - t.Skipf("Adapter for %s does not implement SearchableAdapter", sdpItemType) - } - - // Test Terraform format: projects/[project_id]/locations/[location]/resourceType/[resource_id] - // The adapter should extract the location from this format and search for the specific resource - terraformQuery := fmt.Sprintf("projects/%s/locations/%s/resourceType/%s", projectID, location, resourceName) - sdpItems, err := searchable.Search(ctx, projectID, terraformQuery, true) - if err != nil { - t.Fatalf("Failed to search resources with Terraform format: %v", err) - } - - // The search should return only the specific resource matching the Terraform format - if len(sdpItems) != 1 { - t.Errorf("Expected 1 resource, got %d", len(sdpItems)) - return - } - - // Verify the single item returned - firstItem := sdpItems[0] - if firstItem.GetType() != sdpItemType.String() { - t.Errorf("Expected first item type %s, got %s", sdpItemType.String(), firstItem.GetType()) - } - if firstItem.GetScope() != projectID { - t.Errorf("Expected first item scope '%s', got %s", projectID, firstItem.GetScope()) - } -}) -``` - -## Pointer Helper Functions - -Always define local helper functions for creating pointers: - -```go -func stringPtr(s string) *string { - return &s -} - -func uint64Ptr(u uint64) *uint64 { - return &u -} -``` - -## Common Mistakes to Avoid - -1. **Wrong Package**: Don't use `package main` or `package adapters` -2. **Wrong Protobuf Package**: Using `computepb` for non-Compute resources (e.g., using `computepb.Function` instead of `functionspb.Function`) -3. **Wrong Protobuf API Version**: Not matching the adapter's endpoint version (e.g., using `apiv1` when adapter uses `/v2/`) -4. **Wrong Protobuf Types**: Assuming types exist without verification (e.g., `GlobalAddress` doesn't exist, use `Address`) -5. **Wrong List Response Structure**: Assuming all list responses have `.Items` field (field names vary by service - verify with `go doc`) -6. **Missing Pointer Helpers**: Always define local pointer helper functions -7. **Incorrect HTTP URLs**: Match the exact API endpoint format from the adapter metadata -8. **Missing Static Tests**: Always include blast propagation tests for linked resources -9. **Missing Search Tests**: Include Search tests only if the adapter implements `SearchableAdapter` - use `t.Skipf()` if not supported -10. **Using Pure Strings for ExpectedType**: Always use Item types with `.String()` (e.g., `gcpshared.ComputeNetwork.String()` or `stdlib.NetworkIP.String()`), never pure strings like `"gcp-compute-network"` or `"ip"` - -## Blast Propagation Testing Requirements - -**CRITICAL**: Every adapter test SHOULD include comprehensive StaticTests that cover ALL blast propagation links defined in the adapter configuration. - -### Steps to Ensure Complete Coverage - -1. **Review Adapter File**: Open the corresponding adapter file (e.g., `compute-global-address.go`) -2. **Find blastPropagation Map**: Locate the `blastPropagation` field in the adapter configuration -3. **Create Test Cases**: Write a QueryTest for EVERY entry in the blastPropagation map -4. **Handle TODOs**: Note any blast propagation entries marked with TODO comments - these may not work yet but should be documented in test comments - -### Example Complete StaticTests - -```go -t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - // Network link - { - ExpectedType: gcpshared.ComputeNetwork.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "default", - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - // IP address link - { - ExpectedType: stdlib.NetworkIP.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "203.0.113.1", - ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - // Backend service link - { - ExpectedType: gcpshared.ComputeBackendService.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-backend-service", - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - // Subnetwork link (note special scope format) - { - ExpectedType: gcpshared.ComputeSubnetwork.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-subnet", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) -}) -``` - -## Validation Checklist - -- [ ] Package is `adapters_test` -- [ ] All required imports are present -- [ ] **CRITICAL**: Protobuf package matches the adapter's service (e.g., `aiplatformpb` for AI Platform, NOT `computepb`) -- [ ] **CRITICAL**: Protobuf API version matches the adapter's endpoint (e.g., `/v2/` → `apiv2/functionspb`, `/v1/` → `apiv1/aiplatformpb`) -- [ ] Protobuf types verified with `go doc` command and confirmed to exist -- [ ] List response structure verified (field name like `.Items`, `.Models`, etc. varies by service) -- [ ] Mock HTTP responses match actual API endpoints -- [ ] Get test validates all SDP item properties -- [ ] List test validates item count (expect 2+ items) and properties -- [ ] Search test validates item count (expect 2+ items) and properties (if adapter supports Search) -- [ ] Search with Terraform format test included (if adapter has terraform mappings with Search method) -- [ ] **ErrorHandling test**: Tests error responses (e.g., 404 Not Found) -- [ ] **LIMIT TO 3/4 TEST CASES ONLY**: Get, List/Search, Search with Terraform, ErrorHandling - no additional tests needed -- [ ] Multiple query parameter resources test combined query format (e.g., "location/resource") -- [ ] **CRITICAL**: Static tests include blast propagation for ALL linked resources in the adapter's blastPropagation map -- [ ] Static test queries use correct scope formats (especially for subnetworks: "projectID.region") -- [ ] Static test queries use correct query formats (especially for KMS keys: use `shared.CompositeLookupKey()`) -- [ ] Pointer helper functions are defined locally -- [ ] Test compiles without errors -- [ ] Test runs successfully - -## Key Patterns - -- **SIMPLIFIED TEST STRUCTURE**: Only 3-4 test cases - Get (with StaticTests), List/Search, Search with Terraform format (if applicable), ErrorHandling -- Use `dynamic.MakeAdapter()` to create adapters -- Always validate SDP item type, scope, and unique attribute -- Include comprehensive attribute validation in Get tests using **camelCase** for attribute names -- Use `t.Skipf()` for optional functionality (like Search) -- Always include static tests with blast propagation -- Mock HTTP responses must match actual API endpoints exactly -- **Length checks**: Regular Search/List tests expect 2+ items, but Terraform format Search expects exactly 1 item - -## Post-Implementation Validation - -After completing the adapter and test implementation, you MUST run the following validation steps: - -### 1. Run golangci-lint - -Execute golangci-lint on the sources/gcp directory to check for code quality issues: - -```bash -golangci-lint run ./sources/gcp/... -``` - -**If golangci-lint fails:** - -- Analyze the reported issues carefully -- Fix all linting errors and warnings -- Common issues include: - - Unused variables or imports - - Missing error handling - - Code formatting issues - - Inefficient code patterns -- Re-run golangci-lint until all issues are resolved - -### 2. Run Unit Tests - -Execute the full test suite for the sources/gcp directory: - -```bash -go test -race ./sources/gcp/... -v -``` - -**If tests fail:** - -- Analyze the test failures and error messages -- Common issues include: - - Missing mock responses for HTTP calls - - Incorrect protobuf type usage - - Wrong API endpoint URLs - - Missing or incorrect blast propagation configurations - - Scope format issues (especially for regional resources) -- Fix the underlying issues in the adapter or test code -- Re-run tests until all tests pass - -### 3. Automatic Issue Resolution - -When either golangci-lint or tests fail: - -1. **Analyze the root cause** of each failure -2. **Fix the issues systematically** - don't just suppress warnings -3. **Verify the fix** by re-running the failing command -4. **Repeat until both commands pass successfully** - -### 4. Final Validation - -Both commands must pass before considering the implementation complete: - -- ✅ `golangci-lint run ./sources/gcp/...` (no errors or warnings) -- ✅ `go test -race ./sources/gcp/... -v` (all tests pass) - -**Do not proceed** with any pull request or consider the task complete until both validation steps pass successfully. - -## Test Structure Summary - -```go -TestYourAdapter(t *testing.T) { - // Setup mock data and responses - - t.Run("Get", func(t *testing.T) { - // Test Get functionality - t.Run("StaticTests", func(t *testing.T) { - // Test ALL blast propagation links - }) - }) - - t.Run("Search", func(t *testing.T) { // OR "List" for project-level - // Test Search/List functionality - }) - - t.Run("ErrorHandling", func(t *testing.T) { - // Test error responses - }) -} -``` diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go index a851d1a4..35d2b8e2 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -31,29 +31,20 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631 state // https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.batchPredictionJobs#JobState }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "encryptionSpec.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "model": { ToSDPItemType: gcpshared.AIPlatformModel, Description: "If the Model is deleted or updated: The batch prediction job may fail. If the batch prediction job is updated: The model remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "endpoint": { ToSDPItemType: gcpshared.AIPlatformEndpoint, Description: "If the Endpoint is deleted or updated: The batch prediction job may fail. If the batch prediction job is updated: The endpoint remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: https://linear.app/overmind/issue/ENG-1446/investigate-creating-a-manual-linker-for-cloud-storage "inputConfig.gcsSource.uris": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS source bucket is deleted or inaccessible: The batch prediction job will fail to read input data. If the batch prediction job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: // BigQuery path. For example: bq://projectId.bqDatasetId.bqTableId. @@ -61,17 +52,11 @@ var _ = registerableAdapter{ "inputConfig.bigquerySource.inputUri": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table is deleted or inaccessible: The batch prediction job will fail to read input data. If the batch prediction job is updated: The table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: https://linear.app/overmind/issue/ENG-1446/investigate-creating-a-manual-linker-for-cloud-storage "outputConfig.gcsDestination.outputUriPrefix": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the output GCS bucket is deleted or inaccessible: The batch prediction job will fail to write results. If the batch prediction job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: // BigQuery path. For example: bq://projectId.bqDatasetId.bqTableId. @@ -79,25 +64,16 @@ var _ = registerableAdapter{ "outputConfig.bigqueryDestination.outputUri": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery output table is deleted or inaccessible: The batch prediction job will fail to write results. If the batch prediction job is updated: The table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "serviceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "If the Service Account is deleted or permissions are revoked: The batch prediction job may fail to access required resources. If the batch prediction job is updated: The service account remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "network": gcpshared.ComputeNetworkImpactInOnly, // TODO: https://linear.app/overmind/issue/ENG-1446/investigate-creating-a-manual-linker-for-cloud-storage "unmanagedContainerModel.artifactUri": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS bucket containing the model artifacts is deleted or inaccessible: The batch prediction job will fail to access the model. If the batch prediction job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go index bc70f1f0..52438dcc 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go @@ -22,9 +22,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" "google.golang.org/genproto/googleapis/rpc/status" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -293,7 +293,7 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { } t.Run("StaticTests", func(t *testing.T) { - // Only test blast propagation paths that are currently working + // Only test link rule paths that are currently working // (GCS and BigQuery paths have TODOs and require manual linkers) queryTests := shared.QueryTests{ { @@ -301,30 +301,18 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-model", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.IAMServiceAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("batch-prediction@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Input GCS bucket link { @@ -332,10 +320,6 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("%s-input-bucket", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Output GCS bucket link { @@ -343,10 +327,6 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("%s-output-bucket", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go index 8e6b2dd8..d395c0ad 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -29,78 +29,66 @@ var _ = registerableAdapter{ IAMPermissions: []string{"aiplatform.customJobs.get", "aiplatform.customJobs.list"}, PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The Cloud KMS key that will be used to encrypt the output artifacts. "encryptionSpec.kmsKeyName": { Description: "If the Cloud KMS CryptoKey is updated: The CustomJob may not be able to access encrypted output artifacts. If the CustomJob is updated: The CryptoKey remains unaffected.", ToSDPItemType: gcpshared.CloudKMSCryptoKey, - BlastPropagation: gcpshared.ImpactInOnly, }, // The full name of the network to which the job should be peered. "jobSpec.network": { Description: "If the Compute Network is deleted or updated: The CustomJob may lose connectivity or fail to run as expected. If the CustomJob is updated: The network remains unaffected.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: gcpshared.ImpactInOnly, }, // The service account that the job runs as. "jobSpec.serviceAccount": { Description: "If the IAM Service Account is deleted or updated: The CustomJob may fail to run or lose permissions. If the CustomJob is updated: The service account remains unaffected.", ToSDPItemType: gcpshared.IAMServiceAccount, - BlastPropagation: gcpshared.ImpactInOnly, }, // The Cloud Storage location to store the output of this CustomJob. "jobSpec.baseOutputDirectory.gcsOutputDirectory": { Description: "If the Storage Bucket is deleted or updated: The CustomJob may fail to write outputs. If the CustomJob is updated: The bucket remains unaffected.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The name of a Vertex AI Tensorboard resource to which this CustomJob will upload Tensorboard logs. "jobSpec.tensorboard": { Description: "If the Vertex AI Tensorboard is deleted or updated: The CustomJob may fail to upload logs or lose access to previous logs. If the CustomJob is updated: The tensorboard remains unaffected.", ToSDPItemType: gcpshared.AIPlatformTensorBoard, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The name of an experiment to associate with the CustomJob. "jobSpec.experiment": { Description: "If the Vertex AI Experiment is deleted or updated: The CustomJob may lose experiment tracking or association. If the CustomJob is updated: The experiment remains unaffected.", ToSDPItemType: gcpshared.AIPlatformExperiment, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The name of an experiment run to associate with the CustomJob. "jobSpec.experimentRun": { Description: "If the Vertex AI ExperimentRun is deleted or updated: The CustomJob may lose run tracking or association. If the CustomJob is updated: The experiment run remains unaffected.", ToSDPItemType: gcpshared.AIPlatformExperimentRun, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The name of a model to upload the trained Model to upon job completion. "jobSpec.models": { Description: "If the Vertex AI Model is deleted or updated: The CustomJob may fail to upload or associate the trained model. If the CustomJob is updated: The model remains unaffected.", ToSDPItemType: gcpshared.AIPlatformModel, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The ID of a PersistentResource to run the job on existing machines. "jobSpec.persistentResourceId": { Description: "If the Vertex AI PersistentResource is deleted or updated: The CustomJob may fail to run or lose access to the persistent resources. If the CustomJob is updated: The PersistentResource remains unaffected.", ToSDPItemType: gcpshared.AIPlatformPersistentResource, - BlastPropagation: gcpshared.ImpactInOnly, }, // Container image URI used in worker pool specs (for containerSpec). "jobSpec.workerPoolSpecs.containerSpec.imageUri": { Description: "If the Artifact Registry Docker Image is updated or deleted: The CustomJob may fail to run or use an incorrect container image. If the CustomJob is updated: The Docker image remains unaffected.", ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, - BlastPropagation: gcpshared.ImpactInOnly, }, // Executor container image URI used in worker pool specs (for pythonPackageSpec). "jobSpec.workerPoolSpecs.pythonPackageSpec.executorImageUri": { Description: "If the Artifact Registry Docker Image is updated or deleted: The CustomJob may fail to run or use an incorrect executor image. If the CustomJob is updated: The Docker image remains unaffected.", ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, - BlastPropagation: gcpshared.ImpactInOnly, }, // GCS URIs of Python package files used in worker pool specs. "jobSpec.workerPoolSpecs.pythonPackageSpec.packageUris": { Description: "If the Storage Bucket containing the Python packages is deleted or updated: The CustomJob may fail to access required package files. If the CustomJob is updated: The bucket remains unaffected.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go index 17858ae4..ae50dcb7 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/aiplatform/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -74,10 +74,6 @@ func TestAIPlatformCustomJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // jobSpec.network @@ -85,10 +81,6 @@ func TestAIPlatformCustomJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // jobSpec.serviceAccount @@ -96,10 +88,6 @@ func TestAIPlatformCustomJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "aiplatform-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go index 52eb5458..9b86fb9d 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -27,63 +27,36 @@ var _ = registerableAdapter{ IAMPermissions: []string{"aiplatform.endpoints.get", "aiplatform.endpoints.list"}, PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "encryptionSpec.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "network": gcpshared.ComputeNetworkImpactInOnly, "deployedModels.model": { ToSDPItemType: gcpshared.AIPlatformModel, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "deployedModels.serviceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "If the service account is deleted or its permissions are updated: The DeployedModel may fail to run or access required resources. If the DeployedModel is updated: The service account remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "deployedModels.sharedResources": { ToSDPItemType: gcpshared.AIPlatformDeploymentResourcePool, Description: "If the DeploymentResourcePool is deleted or updated: The DeployedModel may fail to run or lose access to shared resources. If the DeployedModel is updated: The DeploymentResourcePool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "deployedModels.privateEndpoints.serviceAttachment": { ToSDPItemType: gcpshared.ComputeServiceAttachment, Description: "If the Service Attachment is deleted or updated: The DeployedModel's private endpoint connectivity may be disrupted. If the DeployedModel is updated: The Service Attachment remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "modelDeploymentMonitoringJob": { ToSDPItemType: gcpshared.AIPlatformModelDeploymentMonitoringJob, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "dedicatedEndpointDns": { ToSDPItemType: stdlib.NetworkDNS, Description: "The DNS name for the dedicated endpoint. If the Endpoint is deleted, this DNS name will no longer resolve.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "predictRequestResponseLoggingConfig.bigqueryDestination.outputUri": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery Table is deleted or updated, the Endpoint's logging configuration may be affected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go index 40c080a9..df9170ce 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -110,7 +110,7 @@ func TestAIPlatformEndpoint(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // KMS key link @@ -119,10 +119,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Network link { @@ -130,10 +126,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Deployed model link { @@ -141,10 +133,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-model", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Model deployment monitoring job link { @@ -152,10 +140,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-job"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Dedicated endpoint DNS link { @@ -163,10 +147,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-endpoint.aiplatform.googleapis.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // BigQuery table link { @@ -174,10 +154,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test_dataset", "test_table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go index a8e62200..67c3bb66 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -31,64 +31,39 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.modelDeploymentMonitoringJobs#JobState }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "encryptionSpec.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "endpoint": { ToSDPItemType: gcpshared.AIPlatformEndpoint, Description: "They are tightly coupled - monitoring job monitors the endpoint's deployed models.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "modelDeploymentMonitoringObjectiveConfigs.deployedModelId": { ToSDPItemType: gcpshared.AIPlatformModel, Description: "If the Model is deleted or updated: The monitoring job may fail to monitor. If the monitoring job is updated: The model remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "modelMonitoringAlertConfig.notificationChannels": { ToSDPItemType: gcpshared.MonitoringNotificationChannel, Description: "If the Notification Channel is deleted or updated: The monitoring job may fail to send alerts. If the monitoring job is updated: The notification channel remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "bigqueryTables.bigqueryTablePath": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table storing monitoring logs is deleted or inaccessible: The monitoring job may fail to write logs. If the monitoring job is updated: The table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "modelDeploymentMonitoringObjectiveConfigs.objectiveConfig.trainingDataset.gcsSource.uris": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS bucket containing training data is deleted or inaccessible: The monitoring job may fail to compare predictions against training data. If the monitoring job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "modelDeploymentMonitoringObjectiveConfigs.objectiveConfig.trainingDataset.bigquerySource.inputUri": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table containing training data is deleted or inaccessible: The monitoring job may fail to compare predictions against training data. If the monitoring job is updated: The table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "predictInstanceSchemaUri": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS bucket containing the prediction instance schema is deleted or inaccessible: The monitoring job may fail to validate prediction requests. If the monitoring job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "analysisInstanceSchemaUri": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS bucket containing the analysis instance schema is deleted or inaccessible: The monitoring job may fail to perform analysis. If the monitoring job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go index b676b735..d761c5af 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -128,7 +128,7 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { t.Errorf("Expected unique attribute value '%s', got %s", combinedQuery, sdpItem.UniqueAttributeValue()) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // KMS encryption key link @@ -137,10 +137,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // AI Platform Endpoint link (bidirectional) { @@ -148,10 +144,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-endpoint", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Deployed Model ID link (AI Platform Model) { @@ -159,10 +151,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "deployed-model-123", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Notification Channel 1 link { @@ -170,10 +158,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "alert-channel-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Notification Channel 2 link { @@ -181,10 +165,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "alert-channel-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // BigQuery table 1 link (training predict log) { @@ -192,10 +172,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("monitoring_dataset", "training_predict_log"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // BigQuery table 2 link (serving predict log) { @@ -203,10 +179,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("monitoring_dataset", "serving_predict_log"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Training dataset GCS source bucket links { @@ -214,20 +186,12 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "training-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.StorageBucket.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "training-bucket-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Training dataset BigQuery source link { @@ -235,10 +199,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("training_dataset", "training_table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Deployed Model ID link (second model) { @@ -246,10 +206,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "deployed-model-456", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Schema bucket link for predict instance schema { @@ -257,10 +213,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "schema-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Schema bucket link for analysis instance schema { @@ -268,10 +220,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "schema-bucket-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/ai-platform-model.go b/sources/gcp/dynamic/adapters/ai-platform-model.go index 08750104..778fcb23 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -25,39 +25,25 @@ var _ = registerableAdapter{ IAMPermissions: []string{"aiplatform.models.get", "aiplatform.models.list"}, PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "encryptionSpec.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // Container image used for prediction (containerSpec.imageUri). "containerSpec.imageUri": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Docker Image is updated or deleted: The Model may fail to serve predictions. If the Model is updated: The Docker image remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "pipelineJob": { ToSDPItemType: gcpshared.AIPlatformPipelineJob, Description: "If the Pipeline Job is deleted: The Model may not be retrievable. If the Model is updated: The Pipeline Job remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "deployedModels.endpoint": { ToSDPItemType: gcpshared.AIPlatformEndpoint, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // GCS bucket containing the Model artifact and supporting files (artifactUri). "artifactUri": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket containing model artifacts is deleted or its permissions are changed: The Model may fail to load artifacts and serve predictions. If the Model is updated: The Storage Bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/ai-platform-model_test.go b/sources/gcp/dynamic/adapters/ai-platform-model_test.go index 34e67548..5034f43f 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -106,7 +106,7 @@ func TestAIPlatformModel(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // KMS key link @@ -115,10 +115,6 @@ func TestAIPlatformModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Pipeline job link { @@ -126,10 +122,6 @@ func TestAIPlatformModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pipeline", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Deployed model endpoint link { @@ -137,10 +129,6 @@ func TestAIPlatformModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-endpoint", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Storage bucket link (artifactUri) { @@ -148,10 +136,6 @@ func TestAIPlatformModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("%s-model-artifacts", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go index 27c53225..37ac598b 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -20,7 +20,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"aiplatform.pipelineJobs.get", "aiplatform.pipelineJobs.list"}, PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The service account that the pipeline workload runs as (root-level). "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, // The full name of the network to which the job should be peered (root-level). @@ -31,19 +31,16 @@ var _ = registerableAdapter{ "runtimeConfig.gcsOutputDirectory": { Description: "If the Storage Bucket is deleted or updated: The PipelineJob may fail to write outputs. If the PipelineJob is updated: The bucket remains unaffected.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: gcpshared.ImpactInOnly, }, // The network attachment resource that the pipeline job will use for Private Service Connect. "pscInterfaceConfig.networkAttachment": { Description: "If the Compute Network Attachment is deleted or updated: The PipelineJob may lose access to network services via Private Service Connect. If the PipelineJob is updated: The network attachment remains unaffected.", ToSDPItemType: gcpshared.ComputeNetworkAttachment, - BlastPropagation: gcpshared.ImpactInOnly, }, // The schedule resource name, returned if the pipeline is created by the Schedule API. "scheduleName": { Description: "If the Vertex AI Schedule is deleted or updated: The PipelineJob may stop being triggered or may be triggered incorrectly. If the PipelineJob is updated: The schedule remains unaffected.", ToSDPItemType: gcpshared.AIPlatformSchedule, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go index 24e7b7d0..3e2cd358 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/aiplatform/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -72,10 +72,6 @@ func TestAIPlatformPipelineJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "aiplatform-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // network @@ -83,10 +79,6 @@ func TestAIPlatformPipelineJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // encryptionSpec.kmsKeyName @@ -94,10 +86,6 @@ func TestAIPlatformPipelineJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go index 80db140a..855e4f1b 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -24,13 +24,12 @@ var _ = registerableAdapter{ IAMPermissions: []string{"artifactregistry.dockerimages.get", "artifactregistry.dockerimages.list"}, PredefinedRole: "roles/artifactregistry.reader", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // This is a link to its parent resource: ArtifactRegistryRepository // Linker will extract the repository name from the image name. "name": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Artifact Registry Repository is deleted or updated: The Docker Image may become invalid or inaccessible. If the Docker Image is updated: The repository remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go index e78e098c..3833732d 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/api/artifactregistry/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -110,10 +110,6 @@ func TestArtifactRegistryDockerImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, repository), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/artifact-registry-repository.go b/sources/gcp/dynamic/adapters/artifact-registry-repository.go index f40e8f28..7a984451 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-repository.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-repository.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,36 +21,24 @@ var _ = registerableAdapter{ PredefinedRole: "roles/artifactregistry.reader", // HEALTH: Not currently exposed on the Repository resource (no status field providing operational state) }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // Forward link from parent to child via SEARCH // Link to all docker images in this repository "name": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Repository is deleted or updated: All associated Docker Images may become invalid or inaccessible. If a Docker Image is updated: The repository remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, // Link to upstream repositories in virtual repository configuration "virtualRepositoryConfig.upstreamPolicies.repository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If an upstream Artifact Registry Repository is deleted or updated: The virtual repository may fail to serve artifacts from that upstream. If the virtual repository is updated: The upstream repositories remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Link to Secret Manager Secret Version used for remote repository authentication "remoteRepositoryConfig.upstreamCredentials.passwordSecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecretVersion, Description: "If the Secret Manager Secret Version is deleted or its access is revoked: The remote repository may fail to authenticate with upstream sources. If the remote repository is updated: The secret version remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go index b855610a..069d451b 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -25,21 +25,18 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // state: https://cloud.google.com/bigquery/docs/reference/datatransfer/rest/v1/projects.locations.transferConfigs#TransferState }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "destinationDatasetId": { ToSDPItemType: gcpshared.BigQueryDataset, Description: "If the BigQuery Dataset is deleted or updated: The transfer config may fail to write data. If the transfer config is updated: The dataset remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "dataSourceId": { ToSDPItemType: gcpshared.BigQueryDataTransferDataSource, Description: "If the Data Source is deleted or updated: The transfer config may fail to function. If the transfer config is updated: The data source remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "notificationPubsubTopic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: Notifications may fail to be sent. If the transfer config is updated: The Pub/Sub topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "encryptionConfiguration.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "serviceAccountName": gcpshared.IAMServiceAccountImpactInOnly, @@ -50,10 +47,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.BigQueryDataTransferTransferRun, Description: "If the Transfer Config is deleted or updated: All associated transfer runs may become invalid or inaccessible. If a transfer run is updated: The transfer config remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go index f3d3e4ef..b35e51fe 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/bigquery/datatransfer/apiv1/datatransferpb" "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -150,10 +150,10 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { t.Errorf("Expected notificationPubsubTopic field to be '%s', got %s", notificationPubsubTopic, notificationTopic) } - // Include static tests - MUST cover ALL blast propagation links + // Include static tests - MUST cover ALL link rule links t.Run("StaticTests", func(t *testing.T) { - // CRITICAL: Review the adapter's blast propagation configuration and create - // test cases for EVERY linked resource defined in the adapter's blastPropagation map + // CRITICAL: Review the adapter's link rules configuration and create + // test cases for EVERY linked resource defined in the adapter's link rules map queryTests := shared.QueryTests{ // destinationDatasetId link { @@ -161,10 +161,6 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: destinationDatasetId, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // dataSourceId link - NOTE: BigQueryDataTransferDataSource adapter doesn't exist yet // TODO: Add test case when BigQueryDataTransferDataSource adapter is created @@ -174,10 +170,6 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // encryptionConfiguration.kmsKeyName link { @@ -185,10 +177,6 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go index d70cd425..6af259ed 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,21 +21,18 @@ var _ = registerableAdapter{ IAMPermissions: []string{"bigtable.appProfiles.get", "bigtable.appProfiles.list"}, PredefinedRole: "roles/bigtable.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.BigTableAdminInstance, Description: "If the BigTableAdmin Instance is deleted or updated: The AppProfile may become invalid or inaccessible. If the AppProfile is updated: The instance remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "multiClusterRoutingUseAny.clusterIds": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Cluster is deleted or updated: The AppProfile may lose routing capabilities or fail to access data. If the AppProfile is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "singleClusterRouting.clusterId": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Cluster is deleted or updated: The AppProfile may lose routing capabilities or fail to access data. If the AppProfile is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go index 97576094..2cd25def 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -85,10 +85,6 @@ func TestBigTableAdminAppProfile(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add test for singleClusterRouting.clusterId → BigTableAdminCluster // Requires manual linker to combine instance name with cluster ID @@ -122,10 +118,6 @@ func TestBigTableAdminAppProfile(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add tests for multiClusterRoutingUseAny.clusterIds → BigTableAdminCluster // Requires manual linker to combine instance name with cluster IDs diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup.go b/sources/gcp/dynamic/adapters/big-table-admin-backup.go index 7dc0c7e8..accf93b5 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -22,21 +22,18 @@ var _ = registerableAdapter{ IAMPermissions: []string{"bigtable.backups.get", "bigtable.backups.list"}, PredefinedRole: "roles/bigtable.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Cluster is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "sourceTable": { ToSDPItemType: gcpshared.BigTableAdminTable, Description: "If the BigTableAdmin Table is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The table remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "sourceBackup": { ToSDPItemType: gcpshared.BigTableAdminBackup, Description: "If the source Backup is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The source backup remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "encryptionInfo.kmsKeyVersion": gcpshared.CryptoKeyVersionImpactInOnly, }, diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go index 1499c31b..c1c48ec8 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -75,10 +75,6 @@ func TestBigTableAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, clusterName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // sourceTable @@ -86,10 +82,6 @@ func TestBigTableAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, "source-table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // sourceBackup @@ -97,10 +89,6 @@ func TestBigTableAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, clusterName, "source-backup"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // encryptionInfo.kmsKeyVersion @@ -108,10 +96,6 @@ func TestBigTableAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go index 58cbc8c4..29fe249a 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,7 +21,7 @@ var bigTableAdminClusterAdapter = registerableAdapter{ //nolint:unused // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/bigtable/docs/reference/admin/rest/v2/projects.instances.clusters#State }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Customer-managed encryption key protecting data in this cluster. "encryptionConfig.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // This is a backlink to instance. @@ -31,10 +31,6 @@ var bigTableAdminClusterAdapter = registerableAdapter{ //nolint:unused "name": { ToSDPItemType: gcpshared.BigTableAdminInstance, Description: "If the BigTableAdmin Instance is deleted or updated: The Cluster may become invalid or inaccessible. If the Cluster is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, // No Terraform mapping diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go index 118abc28..51eadc8f 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigtable/admin/apiv2/adminpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -172,10 +172,6 @@ func TestBigTableAdminCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "test-keyring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // name field creates a backlink to the BigTable instance @@ -183,10 +179,6 @@ func TestBigTableAdminCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance.go b/sources/gcp/dynamic/adapters/big-table-admin-instance.go index b2c9e399..b1f52589 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,16 +21,12 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // state: https://cloud.google.com/bigtable/docs/reference/admin/rest/v2/projects.instances#State }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Forward link from parent to child via SEARCH // Link to all clusters in this instance (most fundamental infrastructure component) "name": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Instance is deleted or updated: All associated Clusters may become invalid or inaccessible. If a Cluster is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, @@ -41,6 +37,23 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_bigtable_instance.name", }, + // IAM resources for Bigtable Instances. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent instance for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigtable_instance_iam + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigtable_instance_iam_binding.instance", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigtable_instance_iam_member.instance", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigtable_instance_iam_policy.instance", + }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go index 374c819a..2e0fa848 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigtable/admin/apiv2/adminpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -78,10 +78,6 @@ func TestBigTableAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table.go b/sources/gcp/dynamic/adapters/big-table-admin-table.go index 373c63d7..499b3643 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -23,22 +23,19 @@ var _ = registerableAdapter{ IAMPermissions: []string{"bigtable.tables.get", "bigtable.tables.list"}, PredefinedRole: "roles/bigtable.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.BigTableAdminInstance, Description: "If the BigTableAdmin Instance is deleted or updated: The Table may become invalid or inaccessible. If the Table is updated: The instance remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // If this table was restored from another data source (e.g. a backup), this field, restoreInfo, will be populated with information about the restore. "restoreInfo.backupInfo.sourceTable": { ToSDPItemType: gcpshared.BigTableAdminTable, Description: "If the source BigTableAdmin Table is deleted or updated: The restored table may become invalid or inaccessible. If the restored table is updated: The source table remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "restoreInfo.backupInfo.sourceBackup": { ToSDPItemType: gcpshared.BigTableAdminBackup, Description: "If the source BigTableAdmin Backup is deleted or updated: The restored table may become invalid or inaccessible. If the restored table is updated: The source backup remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ @@ -49,6 +46,26 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_SEARCH, TerraformQueryMap: "google_bigtable_table.id", }, + // IAM resources for Bigtable Tables. These are Terraform-only constructs + // (no standalone GCP API resource exists). We use the instance_name + // attribute because the table attribute is a bare name that the SEARCH + // handler would misinterpret as an instance name. Using instance_name + // lists all tables in the affected instance, providing instance-level + // blast radius for table IAM changes. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigtable_table_iam + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigtable_table_iam_binding.instance_name", + }, + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigtable_table_iam_member.instance_name", + }, + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigtable_table_iam_policy.instance_name", + }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go index d469bbda..a8615272 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -75,10 +75,6 @@ func TestBigTableAdminTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // restoreInfo.backupInfo.sourceTable { @@ -86,10 +82,6 @@ func TestBigTableAdminTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, "source-table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // restoreInfo.backupInfo.sourceBackup { @@ -97,10 +89,6 @@ func TestBigTableAdminTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, "test-cluster", "test-backup"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go index f565c697..2c1808e0 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -33,19 +33,14 @@ var _ = registerableAdapter{ // This role is required via ai adapters and it gives this exact permission. PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "projectId": { ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the Cloud Resource Manager Project is deleted or updated: The billing information may become invalid or inaccessible. If the billing info is updated: The project remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "billingAccountName": { ToSDPItemType: gcpshared.CloudBillingBillingAccount, Description: "If the Cloud Billing Billing Account is deleted or updated: The billing information may become invalid or inaccessible. If the billing info is updated: The billing account is impacted as well.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go b/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go index c753b4a9..a94ee7d9 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/api/cloudbilling/v1" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-build-build.go b/sources/gcp/dynamic/adapters/cloud-build-build.go index 4ed11931..80f67df8 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -24,62 +24,52 @@ var _ = registerableAdapter{ IAMPermissions: []string{"cloudbuild.builds.get", "cloudbuild.builds.list"}, PredefinedRole: "roles/cloudbuild.builds.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "source.storageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The Cloud Build may fail to access source files. If the Cloud Build is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "steps.name": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Docker Image is deleted or updated: The Cloud Build may fail to pull the image. If the Cloud Build is updated: The Docker image remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "results.images": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Cloud Build is updated or deleted: The Artifact Registry Docker Images may no longer be valid or accessible. If the Docker Images are updated: The Cloud Build remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, "images": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If any of the images fail to be pushed, the build status is marked FAILURE.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, "logsBucket": { ToSDPItemType: gcpshared.LoggingBucket, Description: "If the Logging Bucket is deleted or updated: The Cloud Build may fail to write logs. If the Cloud Build is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "buildTriggerId": { // The ID of the BuildTrigger that triggered this build, if it was triggered automatically. ToSDPItemType: gcpshared.CloudBuildTrigger, Description: "If the Cloud Build Trigger is deleted or updated: The Cloud Build may not be retriggered as expected. If the Cloud Build is updated: The trigger remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Artifacts storage location (Cloud Storage bucket for build artifacts) "artifacts.objects.location": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket for artifacts is deleted or updated: The Cloud Build may fail to store build artifacts. If the Cloud Build is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Maven artifacts repository in Artifact Registry "artifacts.mavenArtifacts.repository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Artifact Registry Repository for Maven artifacts is deleted or updated: The Cloud Build may fail to store Maven artifacts. If the Cloud Build is updated: The repository remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // NPM packages repository in Artifact Registry "artifacts.npmPackages.repository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Artifact Registry Repository for NPM packages is deleted or updated: The Cloud Build may fail to store NPM packages. If the Cloud Build is updated: The repository remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Python packages repository in Artifact Registry "artifacts.pythonPackages.repository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Artifact Registry Repository for Python packages is deleted or updated: The Cloud Build may fail to store Python packages. If the Cloud Build is updated: The repository remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Secret Manager secrets used in the build (availableSecrets.secretManager[].version) // The version field contains the full path: projects/{project}/secrets/{secret}/versions/{version} @@ -87,13 +77,11 @@ var _ = registerableAdapter{ "availableSecrets.secretManager.version": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or its access is revoked: The Cloud Build may fail to access required secrets during execution. If the Cloud Build is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Worker pool used for the build (same as Cloud Functions - Run Worker Pool) "options.pool.name": { ToSDPItemType: gcpshared.RunWorkerPool, Description: "If the Cloud Run Worker Pool is deleted or misconfigured: The Cloud Build may fail to execute. If the Cloud Build is updated: The worker pool remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // KMS key for encrypting build logs (if using CMEK) "options.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/cloud-build-build_test.go b/sources/gcp/dynamic/adapters/cloud-build-build_test.go index a7bf65a3..ca64382a 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build_test.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/cloudbuild/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -74,10 +74,6 @@ func TestCloudBuildBuild(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // serviceAccount @@ -85,10 +81,6 @@ func TestCloudBuildBuild(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "cloudbuild-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go index ff35a22e..20a2ec76 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -33,7 +33,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"resourcemanager.projects.get"}, PredefinedRole: "roles/resourcemanager.tagViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There are no links for this item type. // TODO: Currently our highest level of scope is the project. // This item has `parent` attribute that refers to organization or folder which are higher level scopes that we don't support yet. diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go index 202013e2..c542ead7 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/api/cloudresourcemanager/v3" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go index d4310b82..32032c42 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -32,6 +32,6 @@ var cloudResourceManagerTagKeyAdapter = registerableAdapter{ //nolint:unused }, PredefinedRole: "roles/resourcemanager.tagViewer", }, - // No blast propagation yet. TagValue already links back to TagKey via parent attribute. - blastPropagation: map[string]*gcpshared.Impact{}, + // No link rules yet. TagValue already links back to TagKey via parent attribute. + linkRules: map[string]*gcpshared.Impact{}, }.Register() diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go index 5556f40a..51fd89cb 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go @@ -9,8 +9,8 @@ import ( resourcemanagerpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -189,8 +189,8 @@ func TestCloudResourceManagerTagKey(t *testing.T) { } } - // Note: Since this adapter doesn't define blast propagation relationships, - // we don't run StaticTests here. The adapter's blastPropagation map is empty, + // Note: Since this adapter doesn't define link rule relationships, + // we don't run StaticTests here. The adapter's link rules map is empty, // which is correct as TagKeys are configuration resources rather than runtime resources. }) diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go index 6d9e0b80..c08b60ad 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -36,14 +36,10 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/resourcemanager.tagViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "parent": { ToSDPItemType: gcpshared.CloudResourceManagerTagKey, Description: "They are tightly coupled", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go index 86f4cd9f..7ad034db 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -81,10 +81,6 @@ func TestCloudResourceManagerTagValue(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: tagKeyID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function.go b/sources/gcp/dynamic/adapters/cloudfunctions-function.go index 56dd6032..a0d4dfc9 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -28,73 +28,41 @@ var cloudFunctionAdapter = registerableAdapter{ //nolint:unused // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/globalForwardingRules#Status => state // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "buildConfig.source.storageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage bucket is deleted or misconfigured: Function deployment may fail. If the function changes: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "buildConfig.sourceProvenance.resolvedStorageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage bucket is deleted or misconfigured: Function deployment may fail. If the function changes: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "buildConfig.workerPool": { ToSDPItemType: gcpshared.RunWorkerPool, Description: "If the Cloud Run Worker Pool is deleted or misconfigured: Function deployment may fail. If the function changes: The worker pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "buildConfig.dockerRepository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Container Repository is deleted or misconfigured: Function deployment may fail. If the function changes: The repository remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "buildConfig.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "serviceConfig.vpcConnector": { ToSDPItemType: gcpshared.VPCAccessConnector, Description: "If the VPC Access Connector is deleted or misconfigured: Function outbound networking may fail. If the function changes: The connector remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "serviceConfig.service": { ToSDPItemType: gcpshared.RunService, Description: "If the Cloud Run Service is deleted or misconfigured: Function execution may fail. If the function changes: The service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "serviceConfig.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, "eventTrigger.trigger": { ToSDPItemType: gcpshared.EventarcTrigger, Description: "If the Eventarc Trigger is deleted or misconfigured: Function event handling may fail. If the function changes: The trigger remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "eventTrigger.pubsubTopic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or misconfigured: Function event handling may fail. If the function changes: The topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "eventTrigger.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, }, diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go index d3427ba2..bd203dfd 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/functions/apiv2/functionspb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -195,10 +195,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test storage bucket link (buildConfig.source.storageSource.bucket) { @@ -206,10 +202,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test service account link (serviceConfig.serviceAccountEmail) { @@ -217,10 +209,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-function@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Pub/Sub topic link (eventTrigger.pubsubTopic) { @@ -228,10 +216,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test event trigger service account link (eventTrigger.serviceAccountEmail) { @@ -239,10 +223,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("event-trigger@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Eventarc trigger link (eventTrigger.trigger) { @@ -250,10 +230,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-trigger"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Cloud Run service link (serviceConfig.service) { @@ -261,10 +237,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-function-service"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Artifact Registry repository link (buildConfig.dockerRepository) { @@ -272,10 +244,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-docker-repo"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Cloud Run Worker Pool link (buildConfig.workerPool) { @@ -283,10 +251,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-worker-pool"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test resolved storage bucket link (buildConfig.sourceProvenance.resolvedStorageSource.bucket) { @@ -294,10 +258,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-resolved-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Note: serviceConfig.vpcConnector test case omitted because gcp-vpc-access-connector adapter doesn't exist } diff --git a/sources/gcp/dynamic/adapters/compute-accelerator-type.go b/sources/gcp/dynamic/adapters/compute-accelerator-type.go index 4bf173d9..f9fe612f 100644 --- a/sources/gcp/dynamic/adapters/compute-accelerator-type.go +++ b/sources/gcp/dynamic/adapters/compute-accelerator-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.acceleratorTypes.get", "compute.acceleratorTypes.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{}, + linkRules: map[string]*gcpshared.Impact{}, terraformMapping: gcpshared.TerraformMapping{ Description: "There is no terraform resource for this type.", }, diff --git a/sources/gcp/dynamic/adapters/compute-disk-type.go b/sources/gcp/dynamic/adapters/compute-disk-type.go index 6e305f36..3d255096 100644 --- a/sources/gcp/dynamic/adapters/compute-disk-type.go +++ b/sources/gcp/dynamic/adapters/compute-disk-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.diskTypes.get", "compute.diskTypes.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{}, + linkRules: map[string]*gcpshared.Impact{}, terraformMapping: gcpshared.TerraformMapping{ Description: "There is no terraform resource for this type.", }, diff --git a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go index ef744fb0..adf87ace 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -27,7 +27,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "interfaces.ipAddress": gcpshared.IPImpactBothWays, "interfaces.ipv6Address": gcpshared.IPImpactBothWays, }, diff --git a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go index cb20f99c..be2726ec 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -85,10 +85,6 @@ func TestComputeExternalVpnGateway(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-firewall.go b/sources/gcp/dynamic/adapters/compute-firewall.go index db9ff41f..22aeaffc 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall.go +++ b/sources/gcp/dynamic/adapters/compute-firewall.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -20,14 +20,10 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.firewalls.get", "compute.firewalls.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "network": { Description: "If the Compute Network is updated: The firewall rules may no longer apply correctly. If the firewall is updated: The network remains unaffected, but its security posture may change.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "sourceServiceAccounts": gcpshared.IAMServiceAccountImpactInOnly, "targetServiceAccounts": gcpshared.IAMServiceAccountImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/compute-firewall_test.go b/sources/gcp/dynamic/adapters/compute-firewall_test.go index a6045c50..aaddcf5d 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall_test.go +++ b/sources/gcp/dynamic/adapters/compute-firewall_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -83,10 +83,6 @@ func TestComputeFirewall(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // sourceServiceAccounts @@ -94,10 +90,6 @@ func TestComputeFirewall(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // targetServiceAccounts @@ -105,10 +97,6 @@ func TestComputeFirewall(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "target-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-global-address.go b/sources/gcp/dynamic/adapters/compute-global-address.go index 25d8623a..dade7ed2 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address.go +++ b/sources/gcp/dynamic/adapters/compute-global-address.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -31,17 +31,13 @@ var computeGlobalAddressAdapter = registerableAdapter{ //nolint:unused // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/globalAddresses#Status => status // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, "network": gcpshared.ComputeNetworkImpactInOnly, "address": gcpshared.IPImpactBothWays, "ipCollection": { ToSDPItemType: gcpshared.ComputePublicDelegatedPrefix, Description: "If the Public Delegated Prefix is deleted or updated: The Global Address may fail to reserve IP addresses from the prefix. If the Global Address is updated: The Public Delegated Prefix remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-global-address_test.go b/sources/gcp/dynamic/adapters/compute-global-address_test.go index fec76123..7fccd453 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-address_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -158,20 +158,12 @@ func TestComputeGlobalAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.12", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go index 0b51b1e8..88f91838 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -31,7 +31,7 @@ var computeGlobalForwardingRuleAdapter = registerableAdapter{ //nolint:unused // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/globalForwardingRules#Status => pscConnectionStatus // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Network reference (global). If the network is changed it may impact the forwarding rule; forwarding rule updates don't impact the network. "network": gcpshared.ComputeNetworkImpactInOnly, "subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, @@ -41,20 +41,12 @@ var computeGlobalForwardingRuleAdapter = registerableAdapter{ //nolint:unused "backendService": { ToSDPItemType: gcpshared.ComputeBackendService, Description: "If the Backend Service is updated or deleted: The forwarding rule routing behavior changes or breaks. If the forwarding rule is updated or deleted: Traffic will stop or be re-routed affecting the backend service load.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Target resource (polymorphic - can be TargetHttpProxy, TargetHttpsProxy, TargetTcpProxy, TargetSslProxy, TargetPool, TargetVpnGateway, or TargetInstance). // The ForwardingRuleTargetLinker function determines the actual target type from the URI. "target": { ToSDPItemType: gcpshared.ComputeTargetHttpProxy, // Default type, but ForwardingRuleTargetLinker will determine actual type from URI Description: "If the target resource (proxy, pool, gateway, or instance) is updated or deleted: The forwarding rule routing behavior changes or breaks. If the forwarding rule is updated or deleted: Traffic will stop or be re-routed affecting the target resource.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go index 5aae12f6..95cb69dd 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -223,50 +223,30 @@ func TestComputeGlobalForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-backend-service", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnet", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetHttpProxy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-target-proxy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check.go b/sources/gcp/dynamic/adapters/compute-http-health-check.go index f95aa589..77146bb2 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -30,7 +30,7 @@ var _ = registerableAdapter{ }, // HTTP health checks are referenced by backend services and target pools for health monitoring. // Updates to health checks can affect traffic distribution and service availability. - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "host": gcpshared.IPImpactBothWays, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go index 008edb94..a3134320 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go @@ -6,9 +6,9 @@ import ( "net/http" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -74,7 +74,7 @@ func TestComputeHttpHealthCheck(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { // Test that DNS names are correctly detected when using IPImpactBothWays - // Even though the blast propagation uses stdlib.NetworkIP, it should detect + // Even though the link rule uses stdlib.NetworkIP, it should detect // that "example.com" is a DNS name and create a DNS link queryTests := shared.QueryTests{ { @@ -82,17 +82,13 @@ func TestComputeHttpHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) // Test with IP address - verify bidirectional detection works - // Even though the blast propagation uses stdlib.NetworkIP, it should detect + // Even though the link rule uses stdlib.NetworkIP, it should detect // that "192.168.1.1" is an IP address and create an IP link t.Run("StaticTestsWithIP", func(t *testing.T) { healthCheckWithIP := map[string]interface{}{ @@ -129,10 +125,6 @@ func TestComputeHttpHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -160,7 +152,7 @@ func TestComputeHttpHealthCheck(t *testing.T) { // Verify that both IP and DNS are in potential links when using IPImpactBothWays // This demonstrates bidirectional behavior: even though we specify stdlib.NetworkIP - // in the blast propagation, both IP and DNS are included in potential links + // in the link rules, both IP and DNS are included in potential links potentialLinksMap := make(map[string]bool) for _, link := range metadata.GetPotentialLinks() { potentialLinksMap[link] = true diff --git a/sources/gcp/dynamic/adapters/compute-instance-template.go b/sources/gcp/dynamic/adapters/compute-instance-template.go index 72dd3c68..592cfecf 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -20,25 +20,19 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.instanceTemplates.get", "compute.instanceTemplates.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // https://cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates/get "properties.networkInterfaces.network": { Description: "If the network is deleted: Resources may experience connectivity changes or disruptions. If the template is deleted: Network itself is not affected.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.networkInterfaces.subnetwork": { Description: "If the (sub)network is deleted: Resources may experience connectivity changes or disruptions. If the template is updated: Subnetwork itself is not affected.", ToSDPItemType: gcpshared.ComputeSubnetwork, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.networkInterfaces.networkIP": { Description: "IP address are always tightly coupled with the Compute Instance Template.", ToSDPItemType: stdlib.NetworkIP, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "properties.networkInterfaces.ipv6Address": gcpshared.IPImpactBothWays, "properties.networkInterfaces.accessConfigs.natIP": gcpshared.IPImpactBothWays, @@ -50,35 +44,24 @@ var _ = registerableAdapter{ "properties.disks.source": { Description: "If the Compute Disk is updated: Instance creation may fail or behave unexpectedly. If the template is deleted: Existing disks can be deleted.", ToSDPItemType: gcpshared.ComputeDisk, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "properties.disks.initializeParams.diskName": { Description: "If the Compute Disk is updated: Instance creation may fail or behave unexpectedly. If the template is deleted: Existing disks can be deleted.", ToSDPItemType: gcpshared.ComputeDisk, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "properties.disks.initializeParams.sourceImage": { Description: "If the Compute Image is updated: Instances created from this template may not boot correctly. If the template is updated: Image is not affected.", ToSDPItemType: gcpshared.ComputeImage, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.initializeParams.diskType": { Description: "If the Compute Disk Type is updated: New instances may fail to provision disks properly. If the template is updated: Disk type is not affected.", ToSDPItemType: gcpshared.ComputeDiskType, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.initializeParams.sourceImageEncryptionKey.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "properties.disks.initializeParams.sourceImageEncryptionKey.kmsKeyServiceAccount": gcpshared.IAMServiceAccountImpactInOnly, "properties.disks.initializeParams.sourceSnapshot": { Description: "If the Compute Snapshot is updated: The template may reference an invalid or incompatible snapshot. If the template is updated: no impact on snapshots.", ToSDPItemType: gcpshared.ComputeSnapshot, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.initializeParams.sourceSnapshotEncryptionKey.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "properties.disks.initializeParams.sourceSnapshotEncryptionKey.kmsKeyServiceAccount": gcpshared.IAMServiceAccountImpactInOnly, @@ -86,57 +69,44 @@ var _ = registerableAdapter{ "properties.disks.initializeParams.storagePool": { Description: "If the Compute Storage Pool is deleted: Disk provisioning for new instances may fail. If the template is updated: Pool is not affected.", ToSDPItemType: gcpshared.ComputeStoragePool, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.diskEncryptionKey.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "properties.disks.diskEncryptionKey.kmsKeyServiceAccount": gcpshared.IAMServiceAccountImpactInOnly, "properties.guestAccelerators.acceleratorType": { Description: "If the Compute Accelerator Type is updated: New instances may misconfigure or fail hardware initialization. If the template is updated: Accelerator is not affected.", ToSDPItemType: gcpshared.ComputeAcceleratorType, - BlastPropagation: gcpshared.ImpactInOnly, }, "sourceInstance": { Description: "If the Compute Instance is updated: The template may reference an invalid or incompatible instance. If the template is deleted: The instance remains unaffected.", ToSDPItemType: gcpshared.ComputeInstance, - BlastPropagation: gcpshared.ImpactInOnly, }, "sourceInstanceParams.diskConfigs.customImage": { Description: "If the Compute Image is updated: Instances created from this template may not boot correctly. If the template is updated: Image is not affected.", ToSDPItemType: gcpshared.ComputeImage, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.networkInterfaces.networkAttachment": { Description: "If the Compute Network Attachment is updated: Instances using the template may lose access to the network services. If the template is deleted: Attachment is not affected.", ToSDPItemType: gcpshared.ComputeNetworkAttachment, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.initializeParams.licenses": { Description: "If the Compute License is updated: New instances may violate license agreements or lose functionality. If the template is updated: License remains unaffected.", ToSDPItemType: gcpshared.ComputeLicense, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.licenses": { Description: "If the Compute License is updated: New instances may violate license agreements or lose functionality. If the template is updated: License remains unaffected.", ToSDPItemType: gcpshared.ComputeLicense, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.reservationAffinity.values": { Description: "If the Compute Reservation is updated: new instances created using it may fail to launch. If the template is updated: no impacts on reservation.", ToSDPItemType: gcpshared.ComputeReservation, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.scheduling.nodeAffinities.values": { Description: "If the Compute Node Group is updated: Placement policies may break for new VMs. If the template is updated: Node affinity rules may change. Changing the affinity might cause new VMs to stop using that Node Group", ToSDPItemType: gcpshared.ComputeNodeGroup, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "properties.serviceAccounts.email": { Description: "If the IAM Service Account is deleted or updated: Instances created from this template may fail to authenticate or access required resources. If the template is updated: The service account remains unaffected.", ToSDPItemType: gcpshared.IAMServiceAccount, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-instance-template_test.go b/sources/gcp/dynamic/adapters/compute-instance-template_test.go index da3482e1..4159b551 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template_test.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -168,10 +168,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.diskName @@ -179,10 +175,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "disk-name", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.disks.source @@ -190,30 +182,18 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/debian-cloud/global/images/family/debian-11", ExpectedScope: "debian-cloud", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: "test-project.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.networkInterfaces.networkIP @@ -221,10 +201,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.240.17.92", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.ipv6Address @@ -232,10 +208,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2600:1901:0:1234::1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.accessConfigs.natIP @@ -243,10 +215,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.240.17.93", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.accessConfigs.externalIpv6 @@ -254,10 +222,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2600:1901:0:1234::2", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.accessConfigs.securityPolicy @@ -265,10 +229,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.networkInterfaces.ipv6AccessConfigs.natIP @@ -276,10 +236,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.240.17.94", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.ipv6AccessConfigs.externalIpv6 @@ -287,10 +243,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2600:1901:0:1234::3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.ipv6AccessConfigs.securityPolicy @@ -298,10 +250,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy-ipv6", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceSnapshot @@ -309,10 +257,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.resourcePolicies @@ -320,10 +264,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-resource-policy", ExpectedScope: "test-project.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.storagePool @@ -331,10 +271,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-storage-pool", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.licenses @@ -342,10 +278,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "debian-11-bullseye-init-param", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.licenses @@ -353,10 +285,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "debian-11-bullseye-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceImageEncryptionKey.kmsKeyName @@ -364,10 +292,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|my-keyring|source-image-encryption-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceImageEncryptionKey.kmsKeyServiceAccount @@ -375,10 +299,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-image-encryption-key-service-account@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.guestAccelerators.acceleratorType @@ -386,10 +306,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nvidia-tesla-t4", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.scheduling.nodeAffinities.values @@ -397,10 +313,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-node-group", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.reservationAffinity.values @@ -408,10 +320,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-reservation", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.diskType @@ -419,10 +327,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceSnapshotEncryptionKey.kmsKeyName @@ -430,10 +334,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|my-keyring|source-snapshot-encryption-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceSnapshotEncryptionKey.kmsKeyServiceAccount @@ -441,10 +341,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-snapshot-encryption-key-service-account@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.diskEncryptionKey.kmsKeyName @@ -452,10 +348,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|my-keyring|disk-encryption-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.diskEncryptionKey.kmsKeyServiceAccount @@ -463,10 +355,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "disk-encryption-key-service-account@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-license.go b/sources/gcp/dynamic/adapters/compute-license.go index 04dbab29..da33d5dd 100644 --- a/sources/gcp/dynamic/adapters/compute-license.go +++ b/sources/gcp/dynamic/adapters/compute-license.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -25,7 +25,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.licenses.get", "compute.licenses.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{}, + linkRules: map[string]*gcpshared.Impact{}, terraformMapping: gcpshared.TerraformMapping{ Description: "There is no terraform resource for this type.", }, diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go index af496af4..a95f640b 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -28,44 +28,28 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Parent VPC network reference (changes to network can impact NEG reachability; NEG changes do not impact network) "network": gcpshared.ComputeNetworkImpactInOnly, // Subnetwork reference (regional) – subnetwork changes can affect endpoints, NEG changes do not affect subnetwork "subnetwork": { ToSDPItemType: gcpshared.ComputeSubnetwork, Description: "If the Compute Subnetwork is updated: Endpoint reachability or configuration for the NEG may change. If the NEG is updated: The subnetwork remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Serverless NEG referencing a Cloud Run Service "cloudRun.service": { ToSDPItemType: gcpshared.RunService, Description: "If the Cloud Run Service is updated or deleted: Requests routed via the NEG may fail or change behavior. If the NEG changes: The Cloud Run service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Serverless NEG referencing an App Engine service "appEngine.service": { ToSDPItemType: gcpshared.AppEngineService, Description: "If the App Engine Service is updated or deleted: Requests routed via the NEG may fail or change behavior. If the NEG changes: The App Engine service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Serverless NEG referencing a Cloud Function "cloudFunction.function": { ToSDPItemType: gcpshared.CloudFunctionsFunction, Description: "If the Cloud Function is updated or deleted: Requests routed via the NEG may fail or change behavior. If the NEG changes: The Cloud Function remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go index 9b4f5db7..a4e723ff 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -98,10 +98,6 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -109,10 +105,6 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Cloud Run service link { @@ -120,10 +112,6 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "test-cloud-run-service"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Note: App Engine service link test omitted because gcp-app-engine-service adapter doesn't exist yet // Cloud Function link @@ -132,10 +120,6 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "test-cloud-function"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-network.go b/sources/gcp/dynamic/adapters/compute-network.go index 6d23683b..753d9fa6 100644 --- a/sources/gcp/dynamic/adapters/compute-network.go +++ b/sources/gcp/dynamic/adapters/compute-network.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -19,28 +19,19 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.networks.get", "compute.networks.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "gatewayIPv4": gcpshared.IPImpactBothWays, "subnetworks": { Description: "If the Compute Subnetwork is deleted: The network remains unaffected, but its subnetwork configuration may change. If the network is deleted: All associated subnetworks are also deleted.", ToSDPItemType: gcpshared.ComputeSubnetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "peerings.network": { Description: "If the Compute Network Peering is deleted: The network remains unaffected, but its peering configuration may change. If the network is deleted: All associated peerings are also deleted.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "firewallPolicy": { Description: "If the Compute Firewall Policy is updated: The network's security posture may change. If the network is updated: The firewall policy remains unaffected, but its application to the network may change.", ToSDPItemType: gcpshared.ComputeFirewallPolicy, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-network_test.go b/sources/gcp/dynamic/adapters/compute-network_test.go index cfd6ca31..4968ad42 100644 --- a/sources/gcp/dynamic/adapters/compute-network_test.go +++ b/sources/gcp/dynamic/adapters/compute-network_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -80,10 +80,6 @@ func TestComputeNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // subnetworks @@ -91,10 +87,6 @@ func TestComputeNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: "test-project.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // peerings.network @@ -102,10 +94,6 @@ func TestComputeNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "peer-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // TODO: Add test for firewallPolicy → ComputeFirewallPolicy // Requires ComputeFirewallPolicy adapter to be implemented first diff --git a/sources/gcp/dynamic/adapters/compute-project.go b/sources/gcp/dynamic/adapters/compute-project.go index 7bb60655..f4b9a0df 100644 --- a/sources/gcp/dynamic/adapters/compute-project.go +++ b/sources/gcp/dynamic/adapters/compute-project.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -40,25 +40,62 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.projects.get"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "defaultServiceAccount": { Description: "If the IAM Service Account is deleted: Project resources may fail to work as before. If the project is deleted: service account is deleted.", ToSDPItemType: gcpshared.IAMServiceAccount, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "usageExportLocation.bucketName": { Description: "If the Compute Bucket is deleted: Project usage export may fail. If the project is deleted: bucket is deleted.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ - Description: "There is no terraform resource for this type.", + Reference: "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project", + Description: "Maps google_project, Shared VPC, and project IAM resources to the Compute Project adapter.", + Mappings: []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project.project_id", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_compute_shared_vpc_host_project.project", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_compute_shared_vpc_service_project.service_project", + }, + { + // Host project is also affected when the attachment is created/destroyed. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_compute_shared_vpc_service_project.host_project", + }, + // IAM resources for Projects. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent project for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam + { + // Authoritative for a given role — grants the role to a list of members. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project_iam_binding.project", + }, + { + // Non-authoritative — grants a single member a single role. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project_iam_member.project", + }, + { + // Authoritative for the entire IAM policy on the project. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project_iam_policy.project", + }, + { + // Configures which services and log types are audited for the project. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project_iam_audit_config.project", + }, + }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/compute-project_test.go b/sources/gcp/dynamic/adapters/compute-project_test.go index cb677599..d475df6d 100644 --- a/sources/gcp/dynamic/adapters/compute-project_test.go +++ b/sources/gcp/dynamic/adapters/compute-project_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -61,10 +61,6 @@ func TestComputeProject(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // usageExportLocation.bucketName @@ -72,10 +68,6 @@ func TestComputeProject(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "usage-export-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go index 4feb6f76..a3788e69 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -36,23 +36,20 @@ var _ = registerableAdapter{ // HEALTH: status (e.g., LIVE/TO_BE_DELETED) may be present on the resource // TODO: https://linear.app/overmind/issue/ENG-631 }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Parent Public Advertised Prefix from which this delegated prefix is allocated. "parentPrefix": { ToSDPItemType: gcpshared.ComputePublicAdvertisedPrefix, Description: "If the Public Advertised Prefix is updated or deleted: the delegated prefix may become invalid or withdrawn. If the delegated prefix changes: the parent advertised prefix remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Each sub-prefix may be delegated to a specific project. "publicDelegatedSubPrefixs.delegateeProject": { ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the delegatee Project is deleted or disabled: usage of the delegated sub-prefix may stop working. If the delegated prefix changes: the project resource remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "publicDelegatedSubPrefixs.name": { ToSDPItemType: gcpshared.ComputePublicDelegatedPrefix, Description: "If the delegated sub-prefix is updated or deleted: usage of the sub-prefix may stop working. If the parent delegated prefix changes: the sub-prefix remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go index de358aae..623ec276 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -99,10 +99,6 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "delegatee-project-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Delegatee project 2 link { @@ -110,10 +106,6 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "delegatee-project-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Sub-prefix 1 link { @@ -121,10 +113,6 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sub-prefix-1", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Sub-prefix 2 link { @@ -132,10 +120,6 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sub-prefix-2", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment.go b/sources/gcp/dynamic/adapters/compute-region-commitment.go index 9ab40008..c3775785 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -22,22 +22,14 @@ var _ = registerableAdapter{ // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/regionCommitments#Status // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "reservations.name": { ToSDPItemType: gcpshared.ComputeReservation, Description: "If the Region Commitment is deleted or updated: Reservations that reference this commitment may lose associated discounts or resource guarantees. If the Reservation is updated or deleted: The commitment remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Changes to reservations don't affect commitments - Out: true, // Changes to commitments affect reservations that reference them - }, }, "licenseResource.license": { ToSDPItemType: gcpshared.ComputeLicense, Description: "If the Region Commitment is deleted or updated: Licenses that reference this commitment won't be affected. If the License is updated or deleted: The commitment may lose associated discounts or resource guarantees.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go index f095c743..a56ef8e8 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -90,20 +90,12 @@ func TestComputeRegionCommitment(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-reservation", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeLicense.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-license", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-resource-policy.go b/sources/gcp/dynamic/adapters/compute-resource-policy.go index 77311d46..25799663 100644 --- a/sources/gcp/dynamic/adapters/compute-resource-policy.go +++ b/sources/gcp/dynamic/adapters/compute-resource-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,14 +21,13 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.resourcePolicies.get", "compute.resourcePolicies.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Cloud Storage bucket storage location where snapshots created by this policy are stored. // The storageLocations field can contain bucket names, gs:// URIs, or region identifiers. // The manual adapter linker will handle extraction of bucket names from various formats. "snapshotSchedulePolicy.snapshotProperties.storageLocations": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The Resource Policy may fail to create snapshots. If the Resource Policy is updated: The Storage Bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-route.go b/sources/gcp/dynamic/adapters/compute-route.go index aa7babeb..3a90e854 100644 --- a/sources/gcp/dynamic/adapters/compute-route.go +++ b/sources/gcp/dynamic/adapters/compute-route.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -20,60 +20,38 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.routes.get", "compute.routes.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // https://cloud.google.com/compute/docs/reference/rest/v1/routes/get // Network that the route belongs to "network": { Description: "If the Compute Network is updated: The route may no longer be valid or correctly associated. If the route is updated: The network remains unaffected, but its routing behavior may change.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Network that the route forwards traffic to, so the relationship will/may be different "nextHopNetwork": { Description: "If the Compute Network is updated: The route may no longer forward traffic properly. If the route is updated: The network remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopIp": { Description: "The network IP address of an instance that should handle matching packets. Tightly coupled with the Compute Route.", ToSDPItemType: stdlib.NetworkIP, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopInstance": { Description: "If the Compute Instance is updated: Routes using it as a next hop may break or change behavior. If the route is deleted: The instance remains unaffected but traffic that was previously using that route will be impacted.", ToSDPItemType: gcpshared.ComputeInstance, - BlastPropagation: gcpshared.ImpactInOnly, }, "nextHopVpnTunnel": { Description: "If the VPN Tunnel is updated: The route may no longer forward traffic properly. If the route is updated: The VPN tunnel remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeVpnTunnel, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopGateway": { Description: "If the Compute Gateway is updated: The route may no longer forward traffic properly. If the route is updated: The gateway remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeGateway, - BlastPropagation: gcpshared.ImpactInOnly, }, "nextHopHub": { // https://cloud.google.com/network-connectivity/docs/reference/networkconnectivity/rest/v1/projects.locations.global.hubs/get Description: "The full resource name of the Network Connectivity Center hub that will handle matching packets. If the hub is updated: The route may no longer forward traffic properly. If the route is updated: The hub remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.NetworkConnectivityHub, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopIlb": { // https://cloud.google.com/compute/docs/reference/rest/v1/routes/get @@ -81,19 +59,11 @@ var _ = registerableAdapter{ // When it's a URL, it references the ForwardingRule. When it's an IP, it's the IP address of the forwarding rule. Description: "The URL to a forwarding rule of type loadBalancingScheme=INTERNAL that should handle matching packets, or the IP address of the forwarding rule. If the Forwarding Rule is updated or deleted: The route may no longer forward traffic properly. If the route is updated: The forwarding rule remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeForwardingRule, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopInterconnectAttachment": { // https://cloud.google.com/compute/docs/reference/rest/v1/routes/get Description: "The URL to an InterconnectAttachment which is the next hop for the route. If the Interconnect Attachment is updated or deleted: The route may no longer forward traffic properly. If the route is updated: The interconnect attachment remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeInterconnectAttachment, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-route_test.go b/sources/gcp/dynamic/adapters/compute-route_test.go index 07707fa6..79a90518 100644 --- a/sources/gcp/dynamic/adapters/compute-route_test.go +++ b/sources/gcp/dynamic/adapters/compute-route_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -76,10 +76,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // nextHopNetwork @@ -87,10 +83,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "peer-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // nextHopIp @@ -98,10 +90,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // nextHopVpnTunnel @@ -109,10 +97,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-tunnel", ExpectedScope: "test-project.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // nextHopInstance @@ -120,10 +104,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add test for nextHopGateway → ComputeGateway // Requires ComputeGateway adapter to be implemented first diff --git a/sources/gcp/dynamic/adapters/compute-router.go b/sources/gcp/dynamic/adapters/compute-router.go index 5093e034..da87dbbc 100644 --- a/sources/gcp/dynamic/adapters/compute-router.go +++ b/sources/gcp/dynamic/adapters/compute-router.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -28,15 +28,11 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.routers.get", "compute.routers.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "network": gcpshared.ComputeNetworkImpactInOnly, "interfaces.linkedInterconnectAttachment": { ToSDPItemType: gcpshared.ComputeInterconnectAttachment, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "interfaces.privateIpAddress": gcpshared.IPImpactBothWays, "interfaces.subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, @@ -47,28 +43,16 @@ var _ = registerableAdapter{ "nats.natIps": { ToSDPItemType: stdlib.NetworkIP, Description: "If the NAT IP address is deleted or updated: The Router NAT may fail to function correctly. If the Router NAT is updated: The IP address remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "nats.drainNatIps": { ToSDPItemType: stdlib.NetworkIP, Description: "If the draining NAT IP address is deleted or updated: The Router NAT may fail to drain correctly. If the Router NAT is updated: The IP address remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "nats.subnetworks.name": gcpshared.ComputeSubnetworkImpactInOnly, "nats.nat64Subnetworks.name": gcpshared.ComputeSubnetworkImpactInOnly, "interfaces.linkedVpnTunnel": { ToSDPItemType: gcpshared.ComputeVpnTunnel, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Child resource: RoutePolicy - Router can list all its route policies via listRoutePolicies // This is a link from parent to child via SEARCH @@ -76,14 +60,10 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ComputeRoutePolicy, Description: "If the Router is deleted or updated: All associated Route Policies may become invalid or inaccessible. If a Route Policy is updated: The router remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, // Router discovers all its Route Policies via SEARCH }, // Note: BgpRoute is also a child resource with listBgpRoutes endpoint, but we can only use "name" - // once in the blastPropagation map. When BgpRoute adapter is created with SEARCH support, + // once in the link rules map. When BgpRoute adapter is created with SEARCH support, // we can consider using a different field or handling it separately. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-router_test.go b/sources/gcp/dynamic/adapters/compute-router_test.go index e402422c..1171e13d 100644 --- a/sources/gcp/dynamic/adapters/compute-router_test.go +++ b/sources/gcp/dynamic/adapters/compute-router_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -133,7 +133,7 @@ func TestComputeRouter(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", routerName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // Network link @@ -142,10 +142,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Interface private IP address { @@ -153,10 +149,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Interface subnetwork { @@ -164,10 +156,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnet", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Interconnect attachment link { @@ -175,10 +163,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-attachment", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // VPN tunnel link { @@ -186,10 +170,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-tunnel", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // BGP peer IP addresses { @@ -197,40 +177,24 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.2", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.4", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // NAT IP addresses { @@ -238,30 +202,18 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.2", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // NAT subnetworks { @@ -269,20 +221,12 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nat-subnet", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nat64-subnet", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-ssl-certificate.go b/sources/gcp/dynamic/adapters/compute-ssl-certificate.go index 95fea2a8..cedebec6 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-certificate.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-certificate.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -29,7 +29,7 @@ var computeSSLCertificateAdapter = registerableAdapter{ //nolint:unused }, }, }, - blastPropagation: map[string]*gcpshared.Impact{ - // There is no blast propagation originating from Compute SSL Certificates + linkRules: map[string]*gcpshared.Impact{ + // There are no link rules originating from Compute SSL Certificates }, }.Register() diff --git a/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go b/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go index 70d81ca1..b73f49ca 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy.go b/sources/gcp/dynamic/adapters/compute-ssl-policy.go index 3626809a..fcb0e8d4 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -27,7 +27,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // SSL Policies are configuration-only resources that define TLS/SSL parameters // They don't have dependencies on other GCP resources, but are referenced by: // - Target HTTPS Proxies (via sslPolicy field) diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go index 712d8bb8..e0ee4f86 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -70,7 +70,7 @@ func TestComputeSSLPolicy(t *testing.T) { t.Errorf("Expected unique attribute value '%s', got %s", policyName, sdpItem.UniqueAttributeValue()) } - // Skip static tests - no blast propagations for this adapter + // Skip static tests - no link rules for this adapter }) t.Run("List", func(t *testing.T) { diff --git a/sources/gcp/dynamic/adapters/compute-storage-pool.go b/sources/gcp/dynamic/adapters/compute-storage-pool.go index e3af0df7..474f44f9 100644 --- a/sources/gcp/dynamic/adapters/compute-storage-pool.go +++ b/sources/gcp/dynamic/adapters/compute-storage-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,24 +21,16 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.storagePools.get", "compute.storagePools.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Link to the storage pool type that defines the characteristics of this storage pool "storagePoolType": { ToSDPItemType: gcpshared.ComputeStoragePoolType, Description: "If the Storage Pool Type is deleted or updated: The Storage Pool may fail to operate correctly or become invalid. If the Storage Pool is updated: The Storage Pool Type remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Link to the zone where the storage pool resides "zone": { ToSDPItemType: gcpshared.ComputeZone, Description: "If the Zone is deleted or becomes unavailable: The Storage Pool may become inaccessible. If the Storage Pool is updated: The Zone remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork.go b/sources/gcp/dynamic/adapters/compute-subnetwork.go index 411e4229..64a1a803 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -20,31 +20,19 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.subnetworks.get", "compute.subnetworks.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "network": { Description: "If the Compute Network is updated: The firewall rules may no longer apply correctly. If the firewall is updated: The network remains unaffected, but its security posture may change.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "gatewayAddress": gcpshared.IPImpactBothWays, "secondaryIpRanges.reservedInternalRange": { Description: "If the Reserved Internal Range is deleted or updated: The subnetwork's secondary IP range configuration may become invalid. If the subnetwork is updated: The internal range remains unaffected.", ToSDPItemType: gcpshared.NetworkConnectivityInternalRange, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "ipCollection": { Description: "If the Public Delegated Prefix is deleted or updated: The subnetwork may lose its IP allocation source (BYOIP). If the subnetwork is updated: The prefix remains unaffected.", ToSDPItemType: gcpshared.ComputePublicDelegatedPrefix, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go index b1292a17..84643afc 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -77,10 +77,6 @@ func TestComputeSubnetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // gatewayAddress @@ -88,10 +84,6 @@ func TestComputeSubnetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go index 07a414c5..20223743 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -27,13 +27,10 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "urlMap": { ToSDPItemType: gcpshared.ComputeUrlMap, Description: "If the URL Map is updated or deleted: The HTTP proxy routing behavior may change or break. If the proxy changes: The URL map remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go index 62c520c4..2f2be523 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -80,10 +80,6 @@ func TestComputeTargetHttpProxy(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-url-map", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go index 27d1d16f..fb4403c5 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -27,34 +27,22 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "urlMap": { ToSDPItemType: gcpshared.ComputeUrlMap, Description: "If the URL Map is updated or deleted: The HTTPS proxy routing behavior may change or break. If the proxy changes: The URL map remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "sslCertificates": { ToSDPItemType: gcpshared.ComputeSSLCertificate, Description: "If the SSL Certificate is updated or deleted: TLS handshakes may fail for the HTTPS proxy. If the proxy changes: The certificate resource remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "sslPolicy": { ToSDPItemType: gcpshared.ComputeSSLPolicy, Description: "If the SSL Policy is updated or deleted: TLS handshakes may fail for the HTTPS proxy. If the proxy changes: The SSL policy resource remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "certificateMap": { ToSDPItemType: gcpshared.CertificateManagerCertificateMap, Description: "If the Certificate Map is updated or deleted: TLS handshakes may fail for the HTTPS proxy. If the proxy changes: The certificate map resource remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go index c8ac996a..6354aa99 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -84,30 +84,18 @@ func TestComputeTargetHttpsProxy(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-url-map", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSSLCertificate.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-cert", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSSLPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-target-pool.go b/sources/gcp/dynamic/adapters/compute-target-pool.go index 00765b77..0479c23c 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -35,21 +35,18 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "instances": { ToSDPItemType: gcpshared.ComputeInstance, Description: "If the Compute Instance is deleted or updated: the pool membership becomes invalid or traffic may fail to reach it. If the pool is updated: the instance remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "healthChecks": { ToSDPItemType: gcpshared.ComputeHealthCheck, Description: "If the Health Check is updated or deleted: health status and traffic distribution may be affected. If the pool is updated: the health check remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "backupPool": { ToSDPItemType: gcpshared.ComputeTargetPool, Description: "If the backup Target Pool is updated or deleted: failover behavior may change. If this pool is updated: the backup pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-target-pool_test.go b/sources/gcp/dynamic/adapters/compute-target-pool_test.go index fd487c1c..aa5fbb2e 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -97,10 +97,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "instance-1", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Instance 2 link { @@ -108,10 +104,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "instance-2", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Health check 1 link { @@ -119,10 +111,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "health-check-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Health check 2 link { @@ -130,10 +118,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "health-check-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Backup pool link { @@ -141,10 +125,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backup-pool", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-url-map.go b/sources/gcp/dynamic/adapters/compute-url-map.go index aa2acba9..26325c9e 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map.go +++ b/sources/gcp/dynamic/adapters/compute-url-map.go @@ -1,17 +1,13 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) var computeBackendImpact = &gcpshared.Impact{ ToSDPItemType: gcpshared.ComputeBackendService, Description: "If the Backend Service or Backend Bucket is updated or deleted: The URL Map's routing behavior may change or break. If the URL Map changes: The backend service or bucket remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } // URL Map (global, project-level) resource. @@ -37,7 +33,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "defaultService": computeBackendImpact, "defaultRouteAction.weightedBackendServices.backendService": computeBackendImpact, "defaultRouteAction.requestMirrorPolicy.backendService": computeBackendImpact, diff --git a/sources/gcp/dynamic/adapters/compute-url-map_test.go b/sources/gcp/dynamic/adapters/compute-url-map_test.go index 3849605b..c997f8bb 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map_test.go +++ b/sources/gcp/dynamic/adapters/compute-url-map_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -159,10 +159,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path matcher default service link { @@ -170,10 +166,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-path-matcher-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path rule service link { @@ -181,10 +173,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-path-rule-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Default route action weighted backend service link { @@ -192,10 +180,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-weighted-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Default route action request mirror backend service link { @@ -203,10 +187,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-mirror-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path matcher default route action weighted backend service link { @@ -214,10 +194,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pm-weighted-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path matcher default route action request mirror backend service link { @@ -225,10 +201,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pm-mirror-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path rule route action weighted backend service link { @@ -236,10 +208,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pr-weighted-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path rule route action request mirror backend service link { @@ -247,10 +215,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pr-mirror-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Route rule service link { @@ -258,10 +222,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-route-rule-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Route rule route action weighted backend service link { @@ -269,10 +229,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-rr-weighted-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Route rule route action request mirror backend service link { @@ -280,10 +236,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-rr-mirror-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go index 3a13fd37..550fffdb 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -28,7 +28,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Network associated with the VPN gateway. "network": gcpshared.ComputeNetworkImpactInOnly, // IP addresses assigned to VPN interfaces (each interface may have an external IP). @@ -38,10 +38,6 @@ var _ = registerableAdapter{ "vpnInterfaces.interconnectAttachment": { ToSDPItemType: gcpshared.ComputeInterconnectAttachment, Description: "If the Interconnect Attachment is deleted or updated: The VPN gateway interface may fail to operate correctly. If the VPN gateway is deleted or updated: The interconnect attachment may become disconnected or unusable. They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go index 5fddfc4a..984becbc 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -91,30 +91,18 @@ func TestComputeVpnGateway(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeInterconnectAttachment.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-attachment", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go index 25754813..18a88c30 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -30,48 +30,28 @@ var _ = registerableAdapter{ // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/vpnTunnels#Status => status // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The peer IP address of the remote VPN gateway. "peerIp": gcpshared.IPImpactBothWays, "targetVpnGateway": { ToSDPItemType: gcpshared.ComputeTargetVpnGateway, Description: "If the Target VPN Gateway (Classic VPN) is deleted or updated: The VPN Tunnel may become invalid or fail to establish connections. If the VPN Tunnel is updated or deleted: The Target VPN Gateway may be affected as tunnels are tightly coupled to their gateway.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "vpnGateway": { ToSDPItemType: gcpshared.ComputeVpnGateway, Description: "If the HA VPN Gateway is deleted or updated: The VPN Tunnel may become invalid or fail to establish connections. If the VPN Tunnel is updated or deleted: The HA VPN Gateway may be affected as tunnels are tightly coupled to their gateway.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "peerExternalGateway": { ToSDPItemType: gcpshared.ComputeExternalVpnGateway, Description: "If the External VPN Gateway is deleted or updated: The VPN Tunnel may fail to establish connections with the peer. If the VPN Tunnel is updated or deleted: The External VPN Gateway remains unaffected, but the tunnel endpoint becomes inactive.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "peerGcpGateway": { ToSDPItemType: gcpshared.ComputeVpnGateway, Description: "If the peer HA VPN Gateway is deleted or updated: The VPN Tunnel may fail to establish VPC-to-VPC connections. If the VPN Tunnel is updated or deleted: The peer HA VPN Gateway remains unaffected, but the tunnel endpoint becomes inactive.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "router": { ToSDPItemType: gcpshared.ComputeRouter, Description: "If the Cloud Router is deleted or updated: The VPN Tunnel may lose dynamic routing capabilities (BGP). If the VPN Tunnel is updated or deleted: The Cloud Router may lose routes advertised through this tunnel.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go index 8a13c352..4ea038f1 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -94,10 +94,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Target VPN Gateway link (Classic VPN) { @@ -105,10 +101,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-target-gateway", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // VPN Gateway link (HA VPN) { @@ -116,10 +108,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-gateway", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Peer External Gateway link { @@ -127,10 +115,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-external-gateway", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Peer GCP Gateway link { @@ -138,10 +122,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-peer-gcp-gateway", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Router link { @@ -149,10 +129,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-router", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/container-cluster.go b/sources/gcp/dynamic/adapters/container-cluster.go index 9141bde3..c637be9a 100644 --- a/sources/gcp/dynamic/adapters/container-cluster.go +++ b/sources/gcp/dynamic/adapters/container-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -32,7 +32,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/container.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "network": gcpshared.ComputeNetworkImpactInOnly, "subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, "nodePools.config.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, @@ -40,12 +40,10 @@ var _ = registerableAdapter{ "nodePools.config.nodeGroup": { ToSDPItemType: gcpshared.ComputeNodeGroup, Description: "If the referenced Node Group is deleted or updated: Node pools backed by it may fail to create or manage nodes. Updates to the node pool will not affect the node group.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "notificationConfig.pubsub.topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the referenced Pub/Sub topic is deleted or updated: Notifications may fail to be sent. Updates to the cluster will not affect the topic.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // The Cloud KMS cryptoKeyVersions to use for signing service account JWTs issued by this cluster. // Format: projects/{project}/locations/{location}/keyRings/{keyring}/cryptoKeys/{cryptoKey}/cryptoKeyVersions/{cryptoKeyVersion} @@ -63,7 +61,6 @@ var _ = registerableAdapter{ "resourceUsageExportConfig.bigqueryDestination.datasetId": { ToSDPItemType: gcpshared.BigQueryDataset, Description: "If the referenced BigQuery dataset is deleted or updated: Resource usage export may fail. Updates to the cluster will not affect the dataset.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // The IP address of this cluster's master endpoint. "endpoint": gcpshared.IPImpactBothWays, @@ -72,10 +69,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ContainerNodePool, Description: "If the Container Cluster is deleted or updated: All associated Node Pools may become invalid or inaccessible. If a Node Pool is updated: The cluster remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/container-cluster_test.go b/sources/gcp/dynamic/adapters/container-cluster_test.go index 0ac983eb..649f5214 100644 --- a/sources/gcp/dynamic/adapters/container-cluster_test.go +++ b/sources/gcp/dynamic/adapters/container-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/container/apiv1/containerpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -135,7 +135,7 @@ func TestContainerCluster(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", clusterName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // Network link @@ -144,10 +144,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -155,10 +151,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account link { @@ -166,10 +158,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-service-account@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Boot disk KMS key link { @@ -177,10 +165,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Node group link { @@ -188,10 +172,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-node-group", ExpectedScope: fmt.Sprintf("%s.%s", projectID, location), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Pub/Sub topic link { @@ -199,10 +179,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account signing key version link { @@ -210,10 +186,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account verification key version link { @@ -221,10 +193,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key", "2"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Control plane disk encryption key link { @@ -232,10 +200,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "control-plane-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // ETCD backup encryption key link { @@ -243,10 +207,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "etcd-backup-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Database encryption key link { @@ -254,10 +214,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "database-encryption-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // BigQuery dataset link { @@ -265,10 +221,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "gke_usage_export", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Master endpoint IP address link { @@ -276,10 +228,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "35.123.45.67", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Forward link to node pools (parent to child) { @@ -287,10 +235,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: combinedQuery, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/container-node-pool.go b/sources/gcp/dynamic/adapters/container-node-pool.go index b90a78de..6028cc23 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool.go +++ b/sources/gcp/dynamic/adapters/container-node-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -35,30 +35,24 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.locations.clusters.nodePools#NodePool.Status }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // This is a backlink to cluster. // Framework will extract the cluster name and create the linked item query with GET "name": { ToSDPItemType: gcpshared.ContainerCluster, Description: "If the Container Cluster is deleted or updated: The Node Pool may become invalid or inaccessible. If the Node Pool is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "config.bootDiskKmsKey": gcpshared.CryptoKeyImpactInOnly, "config.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "config.nodeGroup": { ToSDPItemType: gcpshared.ComputeNodeGroup, Description: "If the node pool is backed by a node group, then changes to the node group may affect the node pool. Changes to the node pool will not affect the node group.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.network": gcpshared.ComputeNetworkImpactInOnly, "config.subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, "instanceGroupUrls": { ToSDPItemType: gcpshared.ComputeInstanceGroupManager, Description: "If the Instance Group Manager is deleted or updated: The Node Pool may fail to create new nodes or become invalid. If the Node Pool is updated: The instance group manager remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/container-node-pool_test.go b/sources/gcp/dynamic/adapters/container-node-pool_test.go index c636ddf7..0b01b9c3 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool_test.go +++ b/sources/gcp/dynamic/adapters/container-node-pool_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/container/apiv1/containerpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -106,10 +106,6 @@ func TestContainerNodePool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, clusterName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // KMS encryption key link { @@ -117,10 +113,6 @@ func TestContainerNodePool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account link { @@ -128,10 +120,6 @@ func TestContainerNodePool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Node group link { @@ -139,10 +127,6 @@ func TestContainerNodePool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: fmt.Sprintf("%s.%s-a", projectID, location), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/dataform-repository.go b/sources/gcp/dynamic/adapters/dataform-repository.go index 9a7fd74d..3f36e4ab 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository.go +++ b/sources/gcp/dynamic/adapters/dataform-repository.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -24,30 +24,26 @@ var _ = registerableAdapter{ IAMPermissions: []string{"dataform.repositories.get", "dataform.repositories.list"}, PredefinedRole: "roles/dataform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The name of the Secret Manager secret version to use as an authentication token for Git operations. Must be in the format projects/*/secrets/*/versions/*. "gitRemoteSettings.authenticationTokenSecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The Dataform Repository may fail to authenticate with the Git remote. If the Dataform Repository is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // The name of the Secret Manager secret version to use as a ssh private key for Git operations. Must be in the format projects/*/secrets/*/versions/*. "gitRemoteSettings.sshAuthenticationConfig.userPrivateKeySecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The Dataform Repository may fail to authenticate with the Git remote. If the Dataform Repository is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Name of the Secret Manager secret version used to interpolate variables into the .npmrc file for package installation operations. "npmrcEnvironmentVariablesSecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The Dataform Repository may fail to install npm packages. If the Dataform Repository is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // The URL of the Git remote repository. Can be HTTPS (e.g., https://github.com/user/repo.git) or SSH (e.g., git@github.com:user/repo.git). "gitRemoteSettings.url": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the Git remote URL becomes inaccessible: The Dataform Repository may fail to sync with the remote. If the Dataform Repository is updated: The Git remote remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // The service account to run workflow invocations under. "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/dataform-repository_test.go b/sources/gcp/dynamic/adapters/dataform-repository_test.go index 49debb92..70518f10 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository_test.go +++ b/sources/gcp/dynamic/adapters/dataform-repository_test.go @@ -8,9 +8,9 @@ import ( dataform "google.golang.org/api/dataform/v1beta1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -71,10 +71,6 @@ func TestDataformRepository(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "dataform-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // kmsKeyName @@ -82,10 +78,6 @@ func TestDataformRepository(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type.go index 9985cc69..7a02e7bd 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -30,7 +30,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/dataplex.catalogViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Based on the AspectType structure from the API documentation, // AspectTypes typically define metadata schemas and don't have direct dependencies // on other GCP resources in their core definition. They are schema definitions diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go index c1691571..fcafe96b 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go @@ -10,8 +10,8 @@ import ( "cloud.google.com/go/dataplex/apiv1/dataplexpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -152,8 +152,8 @@ func TestDataplexAspectType(t *testing.T) { t.Errorf("Expected etag field to be 'BwWWja0YfJA=', got %s", val) } - // Note: Since this adapter doesn't define blast propagation relationships, - // we don't run StaticTests here. The adapter's blastPropagation map is empty, + // Note: Since this adapter doesn't define link rule relationships, + // we don't run StaticTests here. The adapter's link rules map is empty, // which is correct as AspectTypes are schema definitions rather than runtime resources. }) diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan.go b/sources/gcp/dynamic/adapters/dataplex-data-scan.go index 21b9a0d8..71f0ba04 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -31,14 +31,11 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631 state // https://cloud.google.com/dataplex/docs/reference/rest/v1/projects.locations.dataScans#DataScan }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Data source references - can scan various data sources "data.entity": { ToSDPItemType: gcpshared.DataplexEntity, Description: "If the Dataplex Entity is deleted: The data scan will fail to access the data source. If the data scan is updated: The dataplex entity remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "data.resource": { // Note: data.resource can reference either a Storage Bucket (for DataDiscoveryScan) @@ -47,24 +44,15 @@ var _ = registerableAdapter{ // the BigQueryTable linker automatically. ToSDPItemType: gcpshared.StorageBucket, Description: "If the data source (Storage Bucket or BigQuery Table) is deleted or inaccessible: The data scan will fail to access the data source. If the data scan is updated: The data source remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Post-scan action BigQuery table exports "dataQualitySpec.postScanActions.bigqueryExport.resultsTable": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table for exporting data quality scan results is deleted or inaccessible: The post-scan action will fail. If the data scan is updated: The BigQuery table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "dataProfileSpec.postScanActions.bigqueryExport.resultsTable": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table for exporting data profile scan results is deleted or inaccessible: The post-scan action will fail. If the data scan is updated: The BigQuery table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go index 59ce6d78..8f67d5c2 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/dataplex/apiv1/dataplexpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -96,10 +96,6 @@ func TestDataplexDataScan(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: bucketName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Note: data.entity link also exists but DataplexEntity adapter doesn't exist yet } diff --git a/sources/gcp/dynamic/adapters/dataplex-entry-group.go b/sources/gcp/dynamic/adapters/dataplex-entry-group.go index 6c530eb3..7f91ba66 100644 --- a/sources/gcp/dynamic/adapters/dataplex-entry-group.go +++ b/sources/gcp/dynamic/adapters/dataplex-entry-group.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -25,7 +25,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"dataplex.entryGroups.get", "dataplex.entryGroups.list"}, PredefinedRole: "roles/dataplex.catalogViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go b/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go index 07c3a801..a2b71164 100644 --- a/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/dataplex/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go index 7531bf7e..0c6d99b2 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -29,7 +29,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/dataproc.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // AutoscalingPolicies don't directly reference other resources, // but they are referenced by Dataproc clusters via config.autoscalingConfig.policyUri // The reverse relationship is handled in the cluster adapter diff --git a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go index 8ecea33b..1b8c6348 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go @@ -9,8 +9,8 @@ import ( "cloud.google.com/go/dataproc/v2/apiv1/dataprocpb" "google.golang.org/protobuf/types/known/durationpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -120,7 +120,7 @@ func TestDataprocAutoscalingPolicy(t *testing.T) { t.Errorf("Expected unique attribute value '%s', got %s", policyName, sdpItem.UniqueAttributeValue()) } - // Skip static tests - no blast propagations for this adapter + // Skip static tests - no link rules for this adapter }) t.Run("List", func(t *testing.T) { diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster.go b/sources/gcp/dynamic/adapters/dataproc-cluster.go index 3b24daa8..2a073a63 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -31,7 +31,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/dataproc/docs/reference/rest/v1/projects.regions.clusters#clusterstatus }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "config.gceClusterConfig.networkUri": gcpshared.ComputeNetworkImpactInOnly, "config.gceClusterConfig.subnetworkUri": gcpshared.ComputeSubnetworkImpactInOnly, "config.gceClusterConfig.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, @@ -40,92 +40,74 @@ var _ = registerableAdapter{ "config.masterConfig.imageUri": { ToSDPItemType: gcpshared.ComputeImage, Description: "If the Image is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.masterConfig.managedGroupConfig.instanceGroupManagerUri": { ToSDPItemType: gcpshared.ComputeInstanceGroupManager, Description: "If the Instance Group Manager is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.masterConfig.accelerators.acceleratorTypeUri": { ToSDPItemType: gcpshared.ComputeAcceleratorType, Description: "If the Accelerator Type is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.workerConfig.imageUri": { ToSDPItemType: gcpshared.ComputeImage, Description: "If the Image is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.workerConfig.managedGroupConfig.instanceGroupManagerUri": { ToSDPItemType: gcpshared.ComputeInstanceGroupManager, Description: "If the Instance Group Manager is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.workerConfig.accelerators.acceleratorTypeUri": { ToSDPItemType: gcpshared.ComputeAcceleratorType, Description: "If the Accelerator Type is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.secondaryWorkerConfig.imageUri": { ToSDPItemType: gcpshared.ComputeImage, Description: "If the Image is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.secondaryWorkerConfig.managedGroupConfig.instanceGroupManagerUri": { ToSDPItemType: gcpshared.ComputeInstanceGroupManager, Description: "If the Instance Group Manager is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.secondaryWorkerConfig.accelerators.acceleratorTypeUri": { ToSDPItemType: gcpshared.ComputeAcceleratorType, Description: "If the Accelerator Type is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.autoscalingConfig.policyUri": { ToSDPItemType: gcpshared.DataprocAutoscalingPolicy, Description: "If the Autoscaling Policy is deleted or updated: The cluster may fail to scale. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.auxiliaryNodeGroups.nodeGroup.name": { ToSDPItemType: gcpshared.ComputeNodeGroup, Description: "If the Node Group is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.tempBucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The cluster may fail to stage data or logs. If the cluster is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.stagingBucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The cluster may fail to stage job dependencies, configuration files, or job driver console output. If the cluster is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.metastoreConfig.dataprocMetastoreService": { ToSDPItemType: gcpshared.DataprocMetastoreService, Description: "If the Dataproc Metastore Service is deleted or updated: The cluster may lose access to centralized metadata or fail to operate correctly. If the cluster is updated: The metastore service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "virtualClusterConfig.kubernetesClusterConfig.gkeClusterConfig.gkeClusterTarget": { ToSDPItemType: gcpshared.ContainerCluster, Description: "If the GKE Cluster is deleted or updated: The Dataproc virtual cluster may become invalid or inaccessible. If the Dataproc cluster is updated: The GKE cluster remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "virtualClusterConfig.kubernetesClusterConfig.gkeClusterConfig.nodePoolTarget.nodePool": { ToSDPItemType: gcpshared.ContainerNodePool, Description: "If the GKE Node Pool is deleted or updated: The Dataproc virtual cluster may fail to schedule workloads or lose capacity. If the Dataproc cluster is updated: The node pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "virtualClusterConfig.stagingBucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The virtual cluster may fail to stage job dependencies, configuration files, or job driver console output. If the cluster is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "virtualClusterConfig.auxiliaryServicesConfig.sparkHistoryServerConfig.dataprocCluster": { ToSDPItemType: gcpshared.DataprocCluster, Description: "If the Spark History Server Dataproc Cluster is deleted or updated: The cluster may lose access to Spark job history or fail to monitor Spark applications. If the cluster is updated: The Spark History Server cluster remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go index 207c41d0..a9daf0c6 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/dataproc/v2/apiv1/dataprocpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -112,40 +112,24 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default-subnet", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.IAMServiceAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Master config (SEARCH with full ImageUri) { @@ -153,10 +137,6 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/master-dataproc-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Master accelerator { @@ -164,10 +144,6 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nvidia-tesla-t4", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Worker config (SEARCH with full ImageUri) { @@ -175,10 +151,6 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/worker-dataproc-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Secondary worker config (SEARCH with full ImageUri) { @@ -186,30 +158,18 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/secondary-dataproc-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.StorageBucket.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-temp-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.DataprocAutoscalingPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone.go b/sources/gcp/dynamic/adapters/dns-managed-zone.go index 4cf1fd83..2ada968b 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -22,23 +22,15 @@ var _ = registerableAdapter{ IAMPermissions: []string{"dns.managedZones.get", "dns.managedZones.list"}, PredefinedRole: "roles/dns.reader", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "dnsName": { ToSDPItemType: stdlib.NetworkDNS, Description: "Tightly coupled with the DNS Managed Zone.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // nameServers is an array of DNS names assigned to the managed zone (output only) "nameServers": { ToSDPItemType: stdlib.NetworkDNS, Description: "Nameservers assigned to the managed zone are tightly coupled with the DNS Managed Zone.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "privateVisibilityConfig.networks.networkUrl": gcpshared.ComputeNetworkImpactInOnly, // The resource name of the cluster to bind this ManagedZone to. This should be specified in the format like: projects/*/locations/*/clusters/*. @@ -47,7 +39,6 @@ var _ = registerableAdapter{ "privateVisibilityConfig.gkeClusters.gkeClusterName": { ToSDPItemType: gcpshared.ContainerCluster, Description: "If the GKE Container Cluster is deleted or updated: The DNS Managed Zone may lose visibility for that cluster or fail to resolve names. If the DNS Managed Zone is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "forwardingConfig.targetNameServers.ipv4Address": gcpshared.IPImpactBothWays, "forwardingConfig.targetNameServers.ipv6Address": gcpshared.IPImpactBothWays, @@ -59,7 +50,6 @@ var _ = registerableAdapter{ "serviceDirectoryConfig.namespace.namespaceUrl": { ToSDPItemType: gcpshared.ServiceDirectoryNamespace, Description: "If the Service Directory Namespace is deleted or updated: The DNS Managed Zone may lose its association or fail to resolve names. If the DNS Managed Zone is updated: The namespace remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go index 61b791a8..59bf5b46 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/dns/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -91,10 +91,6 @@ func TestDNSManagedZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com.", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // privateVisibilityConfig.networks.networkUrl @@ -102,23 +98,15 @@ func TestDNSManagedZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add test for privateVisibilityConfig.gkeClusters.gkeClusterName → ContainerCluster - // Requires adapter to define BlastPropagation (currently only has ToSDPItemType) + // Link from adapter (ToSDPItemType only) { // forwardingConfig.targetNameServers.ipv4Address ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.10", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // forwardingConfig.targetNameServers.ipv6Address @@ -126,10 +114,6 @@ func TestDNSManagedZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:db8::1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // peeringConfig.targetNetwork.networkUrl @@ -137,10 +121,6 @@ func TestDNSManagedZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "peering-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add test for serviceDirectoryConfig.namespace.namespaceUrl → ServiceDirectoryNamespace // Requires ServiceDirectoryNamespace adapter to be implemented first diff --git a/sources/gcp/dynamic/adapters/essential-contacts-contact.go b/sources/gcp/dynamic/adapters/essential-contacts-contact.go index febac2e8..e2aa3ccd 100644 --- a/sources/gcp/dynamic/adapters/essential-contacts-contact.go +++ b/sources/gcp/dynamic/adapters/essential-contacts-contact.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -31,7 +31,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"essentialcontacts.contacts.get", "essentialcontacts.contacts.list"}, PredefinedRole: "roles/essentialcontacts.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go b/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go index 614c2426..1dd41c5c 100644 --- a/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go +++ b/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/essentialcontacts/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/eventarc-trigger.go b/sources/gcp/dynamic/adapters/eventarc-trigger.go index 35411727..fe6ba4c5 100644 --- a/sources/gcp/dynamic/adapters/eventarc-trigger.go +++ b/sources/gcp/dynamic/adapters/eventarc-trigger.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -30,89 +30,53 @@ var eventarcTriggerAdapter = registerableAdapter{ //nolint:unused }, PredefinedRole: "roles/eventarc.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Service account used by the trigger to invoke the target service "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, // Channel associated with the trigger for event delivery "channel": { ToSDPItemType: gcpshared.EventarcChannel, Description: "If the Eventarc Channel is deleted or updated: The trigger may fail to receive events. If the trigger is updated: The channel remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Cloud Run Service destination "destination.cloudRunService.service": { ToSDPItemType: gcpshared.RunService, Description: "If the Cloud Run Service is deleted or updated: The trigger may fail to deliver events to the service. If the trigger is updated: The service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Cloud Function destination (fully qualified resource name) "destination.cloudFunction": { ToSDPItemType: gcpshared.CloudFunctionsFunction, Description: "If the Cloud Function is deleted or updated: The trigger may fail to deliver events to the function. If the trigger is updated: The function remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // GKE Cluster destination "destination.gke.cluster": { ToSDPItemType: gcpshared.ContainerCluster, Description: "If the GKE Cluster is deleted or updated: The trigger may fail to deliver events to services in the cluster. If the trigger is updated: The cluster remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Workflow destination (fully qualified resource name) "destination.workflow": { ToSDPItemType: gcpshared.WorkflowsWorkflow, Description: "If the Workflow is deleted or updated: The trigger may fail to invoke the workflow. If the trigger is updated: The workflow remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // HTTP endpoint URI (standard library resource) "destination.httpEndpoint.uri": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP endpoint is unavailable or misconfigured: The trigger may fail to deliver events. If the trigger is updated: The HTTP endpoint remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Network Attachment for VPC-internal HTTP endpoints "destination.httpEndpoint.networkConfig.networkAttachment": { ToSDPItemType: gcpshared.ComputeNetworkAttachment, Description: "If the Network Attachment is deleted or updated: The trigger may fail to access VPC-internal HTTP endpoints. If the trigger is updated: The network attachment remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Pub/Sub Topic used as transport mechanism "transport.pubsub.topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The trigger may fail to transport events. If the trigger is updated: The topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Pub/Sub Subscription created and managed by Eventarc (output only) "transport.pubsub.subscription": { ToSDPItemType: gcpshared.PubSubSubscription, Description: "If the Pub/Sub Subscription is deleted or updated: The trigger may fail to receive events from the topic. If the trigger is updated: The subscription may be recreated or updated by Eventarc.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/file-instance.go b/sources/gcp/dynamic/adapters/file-instance.go index 79448222..d4768491 100644 --- a/sources/gcp/dynamic/adapters/file-instance.go +++ b/sources/gcp/dynamic/adapters/file-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -34,14 +34,13 @@ var _ = registerableAdapter{ PredefinedRole: "roles/file.viewer", // TODO: https://linear.app/overmind/issue/ENG-631 => state }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "networks.network": gcpshared.ComputeNetworkImpactInOnly, "networks.ipAddresses": gcpshared.IPImpactBothWays, "kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "fileShares.sourceBackup": { ToSDPItemType: gcpshared.FileBackup, Description: "If the referenced Backup is deleted or updated: Restores or incremental backups may fail. If the File instance is updated: The backup remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/file-instance_test.go b/sources/gcp/dynamic/adapters/file-instance_test.go index 3d5cd341..d09cb5cc 100644 --- a/sources/gcp/dynamic/adapters/file-instance_test.go +++ b/sources/gcp/dynamic/adapters/file-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/filestore/apiv1/filestorepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -112,10 +112,6 @@ func TestFileInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IP address link { @@ -123,10 +119,6 @@ func TestFileInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.100", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // KMS key link { @@ -134,10 +126,6 @@ func TestFileInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/iam-role.go b/sources/gcp/dynamic/adapters/iam-role.go index 8603f99d..bff139b3 100644 --- a/sources/gcp/dynamic/adapters/iam-role.go +++ b/sources/gcp/dynamic/adapters/iam-role.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"iam.roles.get", "iam.roles.list"}, PredefinedRole: "roles/iam.roleViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/iam-role_test.go b/sources/gcp/dynamic/adapters/iam-role_test.go index b08e6d48..058cba6c 100644 --- a/sources/gcp/dynamic/adapters/iam-role_test.go +++ b/sources/gcp/dynamic/adapters/iam-role_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/iam/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/logging-bucket.go b/sources/gcp/dynamic/adapters/logging-bucket.go index 3423d51a..b3bb259c 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket.go +++ b/sources/gcp/dynamic/adapters/logging-bucket.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -27,7 +27,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"logging.buckets.get", "logging.buckets.list"}, PredefinedRole: "roles/logging.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "cmekSettings.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "cmekSettings.kmsKeyVersionName": gcpshared.CryptoKeyVersionImpactInOnly, "cmekSettings.serviceAccountId": gcpshared.IAMServiceAccountImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/logging-bucket_test.go b/sources/gcp/dynamic/adapters/logging-bucket_test.go index 1918a014..563cfa66 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket_test.go +++ b/sources/gcp/dynamic/adapters/logging-bucket_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -74,10 +74,6 @@ func TestLoggingBucket(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // cmekSettings.kmsKeyVersionName @@ -85,10 +81,6 @@ func TestLoggingBucket(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // cmekSettings.serviceAccountId @@ -96,10 +88,6 @@ func TestLoggingBucket(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "cmek-p123456789@gcp-sa-logging.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/logging-link.go b/sources/gcp/dynamic/adapters/logging-link.go index ffc93799..415b4bbc 100644 --- a/sources/gcp/dynamic/adapters/logging-link.go +++ b/sources/gcp/dynamic/adapters/logging-link.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -25,19 +25,14 @@ var _ = registerableAdapter{ IAMPermissions: []string{"logging.links.get", "logging.links.list"}, PredefinedRole: "roles/logging.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.LoggingBucket, Description: "If the Logging Bucket is deleted or updated: The Logging Link may lose its association or fail to function as expected. If the Logging Link is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "bigqueryDataset.datasetId": { Description: "They are tightly coupled with the Logging Link.", ToSDPItemType: gcpshared.BigQueryDataset, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/logging-link_test.go b/sources/gcp/dynamic/adapters/logging-link_test.go index 0795164d..3b333b2f 100644 --- a/sources/gcp/dynamic/adapters/logging-link_test.go +++ b/sources/gcp/dynamic/adapters/logging-link_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -73,10 +73,6 @@ func TestLoggingLink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, bucketName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // bigqueryDataset.datasetId @@ -84,10 +80,6 @@ func TestLoggingLink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test_dataset", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/logging-saved-query.go b/sources/gcp/dynamic/adapters/logging-saved-query.go index e93ca2ca..0e8567bb 100644 --- a/sources/gcp/dynamic/adapters/logging-saved-query.go +++ b/sources/gcp/dynamic/adapters/logging-saved-query.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -27,7 +27,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"logging.queries.getShared", "logging.queries.listShared"}, PredefinedRole: "roles/logging.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/logging-saved-query_test.go b/sources/gcp/dynamic/adapters/logging-saved-query_test.go index 14f86ed9..9fd02880 100644 --- a/sources/gcp/dynamic/adapters/logging-saved-query_test.go +++ b/sources/gcp/dynamic/adapters/logging-saved-query_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/logging/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/models.go b/sources/gcp/dynamic/adapters/models.go index 77a03b42..5ef55932 100644 --- a/sources/gcp/dynamic/adapters/models.go +++ b/sources/gcp/dynamic/adapters/models.go @@ -8,13 +8,13 @@ import ( type registerableAdapter struct { sdpType shared.ItemType meta gcpshared.AdapterMeta - blastPropagation map[string]*gcpshared.Impact + linkRules map[string]*gcpshared.Impact terraformMapping gcpshared.TerraformMapping } func (d registerableAdapter) Register() registerableAdapter { gcpshared.SDPAssetTypeToAdapterMeta[d.sdpType] = d.meta - gcpshared.BlastPropagations[d.sdpType] = d.blastPropagation + gcpshared.LinkRules[d.sdpType] = d.linkRules gcpshared.SDPAssetTypeToTerraformMappings[d.sdpType] = d.terraformMapping return d diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go index ad72573b..6b79d6ac 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -35,16 +35,14 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/monitoring.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "notificationChannels": { ToSDPItemType: gcpshared.MonitoringNotificationChannel, Description: "The notification channels that are used to notify when this alert policy is triggered. If notification channels are deleted, the alert policy will not be able to notify when triggered. If the alert policy is deleted, the notification channels will not be affected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "alertStrategy.notificationChannelStrategy.notificationChannelNames": { ToSDPItemType: gcpshared.MonitoringNotificationChannel, Description: "The notification channels specified in the alert strategy for channel-specific renotification behavior. If these notification channels are deleted, the alert policy will not be able to notify when triggered. If the alert policy is deleted, the notification channels will not be affected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go index e1ef84a7..7c10d7a0 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -104,7 +104,7 @@ func TestMonitoringAlertPolicy(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // Notification channel links @@ -113,20 +113,12 @@ func TestMonitoringAlertPolicy(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-channel-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.MonitoringNotificationChannel.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-channel-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go index 3c00d4ab..a7057142 100644 --- a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go +++ b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -29,7 +29,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"monitoring.dashboards.get", "monitoring.dashboards.list"}, PredefinedRole: "roles/monitoring.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go index 1fa3b6e3..2ed436e8 100644 --- a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/monitoring/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go index e69f1623..7fbce7d6 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -35,19 +35,17 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/monitoring.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // For pubsub type notification channels, the topic label contains the Pub/Sub topic resource name // Format: projects/{project}/topics/{topic} "labels.topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The Notification Channel may fail to send alerts. If the Notification Channel is updated: The topic remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // For webhook_basicauth and webhook_tokenauth type notification channels, the url label contains the HTTP/HTTPS endpoint "labels.url": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP endpoint is unavailable or updated: The Notification Channel may fail to send alerts. If the Notification Channel is updated: The endpoint remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go index 84c59ad9..3057412b 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -95,7 +95,7 @@ func TestMonitoringNotificationChannel(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Skip static tests - no blast propagations for this adapter + // Skip static tests - no link rules for this adapter // Static tests fail when linked queries are nil }) diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy.go b/sources/gcp/dynamic/adapters/orgpolicy-policy.go index 8985dc6d..fd12b69e 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -34,7 +34,7 @@ var orgPolicyPolicyAdapter = registerableAdapter{ //nolint:unused }, PredefinedRole: "roles/orgpolicy.policyViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The name field contains the parent resource identifier (project, folder, or organization) // Format: projects/{project_number}/policies/{constraint} or // folders/{folder_id}/policies/{constraint} or @@ -46,7 +46,6 @@ var orgPolicyPolicyAdapter = registerableAdapter{ //nolint:unused // the actual type (project, folder, or organization) based on the name prefix ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the parent resource (project, folder, or organization) is deleted or updated: The Org Policy may become invalid or inaccessible. If the Org Policy is updated: The parent resource remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Note: spec.rules[].condition.expression contains CEL expressions that may reference // Tag Keys and Tag Values via resource.matchTag() or resource.matchTagId(). diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go index 4b5b1a4d..a476e090 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/orgpolicy/apiv2/orgpolicypb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -88,7 +88,7 @@ func TestOrgPolicyPolicy(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Skip static tests - no blast propagations for this adapter + // Skip static tests - no link rules for this adapter // Static tests fail when linked queries are nil }) diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription.go b/sources/gcp/dynamic/adapters/pubsub-subscription.go index 7b5624fe..93e0c798 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -23,21 +23,18 @@ var _ = registerableAdapter{ IAMPermissions: []string{"pubsub.subscriptions.get", "pubsub.subscriptions.list"}, PredefinedRole: "roles/pubsub.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The Subscription may fail to receive messages. If the Subscription is updated: The topic remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "deadLetterPolicy.deadLetterTopic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Dead Letter Topic is deleted or updated: The Subscription may fail to deliver failed messages. If the Subscription is updated: The dead letter topic remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "pushConfig.pushEndpoint": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP push endpoint is unavailable or updated: The Subscription may fail to deliver messages via push. If the Subscription is updated: The endpoint remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "pushConfig.oidcToken.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, "bigqueryConfig.table": { @@ -45,19 +42,16 @@ var _ = registerableAdapter{ // We have a manual adapter for this. ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery Table is deleted or updated: The Subscription may fail to write data. If the Subscription is updated: The table remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "bigqueryConfig.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, "cloudStorageConfig.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The Subscription may fail to write data. If the Subscription is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "cloudStorageConfig.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, "analyticsHubSubscriptionInfo.subscription": { ToSDPItemType: gcpshared.PubSubSubscription, Description: "If the Pub/Sub Subscription is deleted or updated: The Analytics Hub Subscription may fail to receive messages. If the Analytics Hub Subscription is updated: The Pub/Sub Subscription remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ @@ -67,6 +61,28 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_pubsub_subscription.name", }, + // IAM resources for Pub/Sub Subscriptions. These are Terraform-only + // constructs (no standalone GCP API resource exists for them). When an + // IAM binding/member/policy changes in a Terraform plan, we resolve it + // to the parent subscription so that blast radius analysis can show the + // downstream impact of the access change. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_subscription_iam + { + // Authoritative for a given role — grants the role to a list of members. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_subscription_iam_binding.subscription", + }, + { + // Non-authoritative — grants a single member a single role. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_subscription_iam_member.subscription", + }, + { + // Authoritative for the entire IAM policy on the subscription. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_subscription_iam_policy.subscription", + }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go index 5c381584..d1378ca2 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go @@ -4,13 +4,14 @@ import ( "context" "fmt" "net/http" + "strings" "testing" "google.golang.org/api/pubsub/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -88,10 +89,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // deadLetterPolicy.deadLetterTopic @@ -99,10 +96,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "dead-letter-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // pushConfig.pushEndpoint @@ -110,10 +103,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/push-endpoint", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // pushConfig.oidcToken.serviceAccountEmail @@ -121,10 +110,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("push-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // bigqueryConfig.table @@ -132,10 +117,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test_dataset", "test_table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // bigqueryConfig.serviceAccountEmail @@ -143,10 +124,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("bq-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // cloudStorageConfig.bucket @@ -154,10 +131,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // cloudStorageConfig.serviceAccountEmail @@ -165,10 +138,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("storage-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -218,3 +187,121 @@ func TestPubSubSubscription(t *testing.T) { } }) } + +// TestPubSubSubscriptionIAMTerraformMappings verifies that the IAM Terraform resource +// types (iam_binding, iam_member, iam_policy) are registered as terraform mappings on +// the PubSub Subscription adapter. This is critical because these Terraform-only +// resources don't have their own GCP API — they represent IAM policy changes on the +// parent subscription. Without these mappings, IAM changes would show as "Unsupported" +// in the change analysis UI instead of being resolved to the parent subscription for +// blast radius analysis. +// +// Background: google_pubsub_subscription_iam_binding is an authoritative Terraform +// resource that manages a single role's members on a subscription. When it changes, +// we need to resolve it to the affected subscription so customers see the downstream +// impact (e.g. services that read from the subscription losing access). +func TestPubSubSubscriptionIAMTerraformMappings(t *testing.T) { + // Retrieve the terraform mappings registered for PubSubSubscription + tfMapping, ok := gcpshared.SDPAssetTypeToTerraformMappings[gcpshared.PubSubSubscription] + if !ok { + t.Fatal("Expected PubSubSubscription to have terraform mappings registered, but none were found") + } + + // Build a lookup of terraform type -> query field from the registered mappings. + // This mirrors the logic in cli/tfutils/plan_mapper.go that splits + // TerraformQueryMap on "." to get the terraform type and attribute name. + type mappingInfo struct { + terraformType string + queryField string + method sdp.QueryMethod + } + registeredMappings := make([]mappingInfo, 0, len(tfMapping.Mappings)) + for _, m := range tfMapping.Mappings { + parts := strings.SplitN(m.GetTerraformQueryMap(), ".", 2) + if len(parts) != 2 { + t.Errorf("Invalid TerraformQueryMap format: %q (expected 'type.attribute')", m.GetTerraformQueryMap()) + continue + } + registeredMappings = append(registeredMappings, mappingInfo{ + terraformType: parts[0], + queryField: parts[1], + method: m.GetTerraformMethod(), + }) + } + + // Define the IAM terraform types we expect to be mapped, along with the + // Terraform attribute that identifies the parent subscription. + // All three IAM resource types use "subscription" as the attribute that + // contains the subscription name. + expectedIAMMappings := []struct { + terraformType string + queryField string + method sdp.QueryMethod + description string // documents why this mapping exists, for reviewer clarity + }{ + { + terraformType: "google_pubsub_subscription_iam_binding", + queryField: "subscription", + method: sdp.QueryMethod_GET, + description: "Authoritative for a given role — maps to parent subscription for blast radius", + }, + { + terraformType: "google_pubsub_subscription_iam_member", + queryField: "subscription", + method: sdp.QueryMethod_GET, + description: "Non-authoritative single member — maps to parent subscription for blast radius", + }, + { + terraformType: "google_pubsub_subscription_iam_policy", + queryField: "subscription", + method: sdp.QueryMethod_GET, + description: "Authoritative for full IAM policy — maps to parent subscription for blast radius", + }, + } + + for _, expected := range expectedIAMMappings { + t.Run(expected.terraformType, func(t *testing.T) { + found := false + for _, registered := range registeredMappings { + if registered.terraformType == expected.terraformType { + found = true + + if registered.queryField != expected.queryField { + t.Errorf("Terraform type %s: expected query field %q, got %q", + expected.terraformType, expected.queryField, registered.queryField) + } + + if registered.method != expected.method { + t.Errorf("Terraform type %s: expected method %s, got %s", + expected.terraformType, expected.method, registered.method) + } + break + } + } + + if !found { + t.Errorf("Terraform type %s is not registered as a mapping on PubSubSubscription. "+ + "This means %q changes will show as 'Unsupported' in the change analysis UI. "+ + "Purpose: %s", + expected.terraformType, expected.terraformType, expected.description) + } + }) + } + + // Also verify the base subscription mapping still exists (sanity check) + t.Run("google_pubsub_subscription", func(t *testing.T) { + found := false + for _, registered := range registeredMappings { + if registered.terraformType == "google_pubsub_subscription" { + found = true + if registered.queryField != "name" { + t.Errorf("Expected query field 'name' for google_pubsub_subscription, got %q", registered.queryField) + } + break + } + } + if !found { + t.Error("Base terraform mapping for google_pubsub_subscription is missing — this would break all subscription change analysis") + } + }) +} diff --git a/sources/gcp/dynamic/adapters/pubsub-topic.go b/sources/gcp/dynamic/adapters/pubsub-topic.go index 5af7d57a..8501f2de 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" @@ -23,67 +23,53 @@ var _ = registerableAdapter{ IAMPermissions: []string{"pubsub.topics.get", "pubsub.topics.list"}, PredefinedRole: "roles/pubsub.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // Schema settings for message validation "schemaSettings.schema": { ToSDPItemType: gcpshared.PubSubSchema, Description: "If the Pub/Sub Schema is deleted or updated: The Topic may fail to validate messages. If the Topic is updated: The schema remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Settings for ingestion from a data source into this topic. "ingestionDataSourceSettings.cloudStorage.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsKinesis.streamArn": { ToSDPItemType: aws.KinesisStream, Description: "The Kinesis stream ARN to ingest data from. If the Kinesis stream is deleted or updated: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The stream remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsKinesis.consumerArn": { ToSDPItemType: aws.KinesisStreamConsumer, Description: "The Kinesis consumer ARN used for ingestion in Enhanced Fan-Out mode. The consumer must be already created and ready to be used. If the consumer is deleted or updated: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The consumer remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsKinesis.awsRoleArn": { ToSDPItemType: aws.IAMRole, Description: "AWS role to be used for Federated Identity authentication with Kinesis. If the AWS IAM role is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The role remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsKinesis.gcpServiceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "GCP service account used for federated identity authentication with AWS Kinesis. If the service account is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The service account remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsMsk.clusterArn": { ToSDPItemType: aws.MSKCluster, Description: "AWS MSK cluster ARN to ingest data from. If the MSK cluster is deleted or updated: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsMsk.awsRoleArn": { ToSDPItemType: aws.IAMRole, Description: "AWS role to be used for Federated Identity authentication with AWS MSK. If the AWS IAM role is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The role remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsMsk.gcpServiceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "GCP service account used for federated identity authentication with AWS MSK. If the service account is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The service account remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.confluentCloud.bootstrapServer": { ToSDPItemType: stdlib.NetworkDNS, Description: "Confluent Cloud bootstrap server endpoint (hostname:port). The linker automatically detects whether the value is a DNS name or IP address and creates the appropriate link. If the bootstrap server is unreachable: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The bootstrap server remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "ingestionDataSourceSettings.confluentCloud.gcpServiceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "GCP service account used for federated identity authentication with Confluent Cloud. If the service account is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The service account remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ @@ -93,6 +79,26 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_pubsub_topic.name", }, + // IAM resources for Pub/Sub Topics. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent topic for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_topic_iam + { + // Authoritative for a given role — grants the role to a list of members. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_topic_iam_binding.topic", + }, + { + // Non-authoritative — grants a single member a single role. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_topic_iam_member.topic", + }, + { + // Authoritative for the entire IAM policy on the topic. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_topic_iam_policy.topic", + }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/pubsub-topic_test.go b/sources/gcp/dynamic/adapters/pubsub-topic_test.go index ee391d40..a88b0302 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/pubsub/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -73,10 +73,6 @@ func TestPubSubTopic(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // ingestionDataSourceSettings.cloudStorage.bucket @@ -84,10 +80,6 @@ func TestPubSubTopic(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "ingestion-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add tests for AWS Kinesis ingestion settings (streamAr, consumerArn, awsRoleArn) // Requires cross-cloud linking setup diff --git a/sources/gcp/dynamic/adapters/redis-instance.go b/sources/gcp/dynamic/adapters/redis-instance.go index 47172a70..de20dac9 100644 --- a/sources/gcp/dynamic/adapters/redis-instance.go +++ b/sources/gcp/dynamic/adapters/redis-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -39,7 +39,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/memorystore/docs/redis/reference/rest/v1/projects.locations.instances#Instance.State }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The name of the VPC network to which the instance is connected. "authorizedNetwork": gcpshared.ComputeNetworkImpactInOnly, // Optional. The KMS key reference that the customer provides when trying to create the instance. @@ -52,7 +52,6 @@ var _ = registerableAdapter{ "serverCaCerts.cert": { ToSDPItemType: gcpshared.ComputeSSLCertificate, Description: "If the certificate is deleted or updated: The Redis instance may lose secure connectivity. If the Redis instance is updated: The certificate remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/redis-instance_test.go b/sources/gcp/dynamic/adapters/redis-instance_test.go index be69192e..19066bea 100644 --- a/sources/gcp/dynamic/adapters/redis-instance_test.go +++ b/sources/gcp/dynamic/adapters/redis-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/redis/apiv1/redispb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -106,7 +106,7 @@ func TestRedisInstance(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // Authorized network link @@ -115,10 +115,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Customer managed key link { @@ -126,10 +122,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Host IP address link { @@ -137,10 +129,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.100", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Read endpoint IP address link { @@ -148,10 +136,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.101", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Server CA certificate link { @@ -159,10 +143,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "-----BEGIN CERTIFICATE-----\nMIIC...test certificate data...\n-----END CERTIFICATE-----", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/run-revision.go b/sources/gcp/dynamic/adapters/run-revision.go index 8ecd8526..ca9aa0ec 100644 --- a/sources/gcp/dynamic/adapters/run-revision.go +++ b/sources/gcp/dynamic/adapters/run-revision.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -29,59 +29,49 @@ var _ = registerableAdapter{ IAMPermissions: []string{"run.revisions.get", "run.revisions.list"}, PredefinedRole: "roles/run.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "service": { ToSDPItemType: gcpshared.RunService, Description: "If the Run Service is deleted or updated: The Revision may lose its association or fail to run. If the Revision is updated: The service remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "vpcAccess.networkInterfaces.network": { ToSDPItemType: gcpshared.ComputeNetwork, Description: "If the Compute Network is deleted or updated: The Revision may lose connectivity or fail to run as expected. If the Revision is updated: The network remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "vpcAccess.networkInterfaces.subnetwork": { ToSDPItemType: gcpshared.ComputeSubnetwork, Description: "If the Compute Subnetwork is deleted or updated: The Revision may lose connectivity or fail to run as expected. If the Revision is updated: The subnetwork remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "vpcAccess.connector": { ToSDPItemType: gcpshared.VPCAccessConnector, Description: "If the VPC Access Connector is deleted or updated: The Revision may lose connectivity or fail to run as expected. If the Revision is updated: The connector remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "containers.image": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Docker Image is deleted or updated: The Revision may fail to pull the image. If the Revision is updated: The Docker image remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "volumes.cloudSqlInstance.instances": { // Format: {project}:{location}:{instance} // The manual adapter linker handles this format automatically. ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The Revision may fail to access the database. If the Revision is updated: The instance remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "volumes.gcs.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The Revision may fail to access the GCS volume. If the Revision is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "volumes.secret.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The Revision may fail to access sensitive data mounted as a volume. If the Revision is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "volumes.nfs.server": { ToSDPItemType: stdlib.NetworkIP, Description: "If the NFS server (IP address or hostname) becomes unavailable: The Revision may fail to mount the NFS volume. If the Revision is updated: The NFS server remains unaffected. The linker automatically detects whether the value is an IP address or DNS name.", - BlastPropagation: gcpshared.ImpactInOnly, }, "logUri": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the log URI endpoint becomes unavailable: The Revision logs may not be accessible. If the Revision is updated: The log URI endpoint remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "encryptionKey": gcpshared.CryptoKeyImpactInOnly, }, diff --git a/sources/gcp/dynamic/adapters/run-revision_test.go b/sources/gcp/dynamic/adapters/run-revision_test.go index d0624fb4..b5a813b5 100644 --- a/sources/gcp/dynamic/adapters/run-revision_test.go +++ b/sources/gcp/dynamic/adapters/run-revision_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/run/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -72,10 +72,6 @@ func TestRunRevision(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, serviceName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // serviceAccount @@ -83,10 +79,6 @@ func TestRunRevision(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "run-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/run-service.go b/sources/gcp/dynamic/adapters/run-service.go index 10b8cd12..edbbefbc 100644 --- a/sources/gcp/dynamic/adapters/run-service.go +++ b/sources/gcp/dynamic/adapters/run-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -30,73 +30,58 @@ var _ = registerableAdapter{ PredefinedRole: "roles/run.viewer", // TODO: https://linear.app/overmind/issue/ENG-631 - status field for health monitoring }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "template.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "template.vpcAccess.connector": { ToSDPItemType: gcpshared.VPCAccessConnector, Description: "If the VPC Access Connector is deleted or updated: The service may lose connectivity or fail to route traffic correctly. If the service is updated: The connector remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.vpcAccess.networkInterfaces.network": { ToSDPItemType: gcpshared.ComputeNetwork, Description: "If the Compute Network is deleted or updated: The service may lose connectivity or fail to route traffic correctly. If the service is updated: The network remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.vpcAccess.networkInterfaces.subnetwork": { ToSDPItemType: gcpshared.ComputeSubnetwork, Description: "If the Compute Subnetwork is deleted or updated: The service may lose connectivity or fail to route traffic correctly. If the service is updated: The subnetwork remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.containers.image": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Docker Image is deleted or updated: The service may fail to deploy new revisions. If the service is updated: The Docker image remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.containers.env.valueSource.secretKeyRef.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the referenced Secret Manager Secret is deleted or updated: the container may fail to start or access sensitive configuration. If the service is updated: the secret remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.volumes.secret.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The service may fail to access sensitive data. If the service is updated: The secret remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.volumes.cloudSqlInstance.instances": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The service may fail to access the database. If the service is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.volumes.gcs.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The service may fail to access stored data. If the service is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.encryptionKey": gcpshared.CryptoKeyImpactInOnly, "latestCreatedRevision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Service is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The service status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, "latestReadyRevision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Service is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The service status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, "traffic.revision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Service is deleted or updated: Traffic allocation to revisions will be lost. If revisions are updated: The service traffic configuration may need updates.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Forward link from parent to child via SEARCH // Link to all revisions in this service "name": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Service is deleted or updated: All associated Revisions may become invalid or inaccessible. If a Revision is updated: The service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, // Link to Binary Authorization platform policy (when explicitly specified via policy field) @@ -105,31 +90,21 @@ var _ = registerableAdapter{ "binaryAuthorization.policy": { ToSDPItemType: gcpshared.BinaryAuthorizationPlatformPolicy, Description: "If the Binary Authorization platform policy is updated: The service may fail to deploy new revisions if images don't meet policy requirements. If the service is updated: The policy remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Link to Cloud Storage bucket used in buildConfig source (if buildConfig is used) "buildConfig.source.storageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket containing source code is deleted or updated: The service may fail to build new revisions. If the service is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Link to HTTP/HTTPS URLs serving traffic for this service "urls": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP endpoint becomes unavailable: The service cannot serve traffic. If the service is updated: The endpoint URL may change.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Link to main URI serving traffic for this service "uri": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP endpoint becomes unavailable: The service cannot serve traffic. If the service is updated: The endpoint URI may change.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/run-service_test.go b/sources/gcp/dynamic/adapters/run-service_test.go index 0407ec00..5d57ff4b 100644 --- a/sources/gcp/dynamic/adapters/run-service_test.go +++ b/sources/gcp/dynamic/adapters/run-service_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/run/apiv2/runpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -156,10 +156,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.vpcAccess.networkInterfaces.network { @@ -167,10 +163,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.vpcAccess.networkInterfaces.subnetwork { @@ -178,10 +170,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.%s", projectID, location), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.containers.env.valueSource.secretKeyRef.secret { @@ -189,10 +177,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "api-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.volumes.secret.secret { @@ -200,10 +184,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "db-creds", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.volumes.cloudSqlInstance.instances { @@ -211,10 +191,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-db", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.volumes.gcs.bucket { @@ -222,10 +198,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.encryptionKey { @@ -233,10 +205,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // latestReadyRevision { @@ -244,10 +212,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, serviceName, "rev-1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // latestCreatedRevision { @@ -255,10 +219,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, serviceName, "rev-2"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // traffic.revision { @@ -266,10 +226,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, serviceName, "rev-3"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // name (parent to child search) { @@ -277,10 +233,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: shared.CompositeLookupKey(location, serviceName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/run-worker-pool.go b/sources/gcp/dynamic/adapters/run-worker-pool.go index d06d1f5c..937cc94a 100644 --- a/sources/gcp/dynamic/adapters/run-worker-pool.go +++ b/sources/gcp/dynamic/adapters/run-worker-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -31,7 +31,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/run.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Service account used by revisions in the worker pool "template.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, // Encryption key for image encryption @@ -40,88 +40,71 @@ var _ = registerableAdapter{ "template.vpcAccess.connector": { ToSDPItemType: gcpshared.VPCAccessConnector, Description: "If the VPC Access Connector is deleted or updated: The worker pool may lose connectivity or fail to route traffic correctly. If the worker pool is updated: The connector remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // VPC Network for direct VPC egress "template.vpcAccess.networkInterfaces.network": { ToSDPItemType: gcpshared.ComputeNetwork, Description: "If the Compute Network is deleted or updated: The worker pool may lose connectivity or fail to route traffic correctly. If the worker pool is updated: The network remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // VPC Subnetwork for direct VPC egress "template.vpcAccess.networkInterfaces.subnetwork": { ToSDPItemType: gcpshared.ComputeSubnetwork, Description: "If the Compute Subnetwork is deleted or updated: The worker pool may lose connectivity or fail to route traffic correctly. If the worker pool is updated: The subnetwork remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Service Mesh for advanced networking "template.serviceMesh.mesh": { ToSDPItemType: gcpshared.NetworkServicesMesh, Description: "If the Network Services Mesh is deleted or updated: The worker pool may lose service mesh connectivity or fail to communicate with other mesh services. If the worker pool is updated: The mesh remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Secret Manager secrets mounted as volumes "template.volumes.secret.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The worker pool may fail to access sensitive data mounted as volumes. If the worker pool is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Cloud SQL instances mounted as volumes "template.volumes.cloudSqlInstance.instances": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The worker pool may fail to access the database. If the worker pool is updated: The instance remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // GCS buckets mounted as volumes "template.volumes.gcs.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The worker pool may fail to access stored data. If the worker pool is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // NFS server (IP address or DNS name) - auto-detected by linker "template.volumes.nfs.server": { ToSDPItemType: stdlib.NetworkIP, Description: "If the NFS server (IP address or hostname) becomes unavailable: The worker pool may fail to mount the NFS volume. If the worker pool is updated: The NFS server remains unaffected. The linker automatically detects whether the value is an IP address or DNS name.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Secret Manager secrets used in environment variables "template.containers.env.valueSource.secretKeyRef.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the referenced Secret Manager Secret is deleted or updated: The container may fail to start or access sensitive configuration. If the worker pool is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Binary Authorization policy "binaryAuthorization.policy": { ToSDPItemType: gcpshared.BinaryAuthorizationPlatformPolicy, Description: "If the Binary Authorization policy is deleted or updated: The worker pool may fail to deploy new revisions if they don't meet policy requirements. If the worker pool is updated: The policy remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Latest ready revision - child resource "latestReadyRevision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Worker Pool is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The worker pool status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Latest created revision - child resource "latestCreatedRevision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Worker Pool is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The worker pool status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Instance split revisions - child resources "instanceSplits.revision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Worker Pool is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The worker pool status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Forward link from parent to child via SEARCH - discover all revisions in this worker pool "name": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Worker Pool is deleted or updated: All associated Revisions may become invalid or inaccessible. If a Revision is updated: The worker pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret.go b/sources/gcp/dynamic/adapters/secret-manager-secret.go index 2edcbe49..223f0cd0 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -32,7 +32,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/secretmanager.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // CMEK used with Automatic replication "replication.automatic.customerManagedEncryption.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // CMEK used with User-managed replication per replica @@ -41,10 +41,6 @@ var _ = registerableAdapter{ "topics.name": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or its policy changes: Secret event notifications may fail. If the Secret changes: The topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go index ad0720c9..4a6f9924 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -135,10 +135,6 @@ func TestSecretManagerSecret(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // topics.name { @@ -146,10 +142,6 @@ func TestSecretManagerSecret(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "secret-events", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -184,10 +176,6 @@ func TestSecretManagerSecret(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "region-ring", "region-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go index 948636e5..722cecec 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -30,7 +30,7 @@ var _ = registerableAdapter{ PredefinedRole: "roles/securitycentermanagement.viewer", // TODO: https://linear.app/overmind/issue/ENG-631 - check if SecurityCenterService has status/state attribute }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Link to parent resource (project, folder, or organization) from name field // The name field format is: projects/{project}/locations/{location}/securityCenterServices/{service} // or: folders/{folder}/locations/{location}/securityCenterServices/{service} @@ -40,10 +40,6 @@ var _ = registerableAdapter{ "name": { Description: "If the parent Project, Folder, or Organization is deleted or updated: The Security Center Service may become invalid or inaccessible. If the Security Center Service is updated: The parent resource remains unaffected.", ToSDPItemType: gcpshared.CloudResourceManagerProject, // Manual linker handles detection of project/folder/organization from name prefix - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Note: Custom modules (SecurityHealthAnalyticsCustomModule, EventThreatDetectionCustomModule, etc.) // are not direct children in the API path structure - they are sibling resources under the same diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go index 5a144d85..442c70d9 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/securitycentermanagement/apiv1/securitycentermanagementpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -83,10 +83,6 @@ func TestSecurityCenterManagementSecurityCenterService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: projectID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint.go b/sources/gcp/dynamic/adapters/service-directory-endpoint.go index d5daee47..906cb0a4 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -23,11 +23,10 @@ var _ = registerableAdapter{ IAMPermissions: []string{"servicedirectory.endpoints.get", "servicedirectory.endpoints.list"}, PredefinedRole: "roles/servicedirectory.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.ServiceDirectoryService, Description: "If the Service Directory Service is deleted or updated: The Endpoint may lose its association or fail to resolve names. If the Endpoint is updated: The service remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // An IPv4 or IPv6 address. "address": gcpshared.IPImpactBothWays, diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go index 0152f300..96ba6461 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/servicedirectory/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -74,10 +74,6 @@ func TestServiceDirectoryEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, namespace, serviceName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // address @@ -85,10 +81,6 @@ func TestServiceDirectoryEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // network @@ -96,10 +88,6 @@ func TestServiceDirectoryEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/service-directory-service.go b/sources/gcp/dynamic/adapters/service-directory-service.go index 2fd12cac..7470338d 100644 --- a/sources/gcp/dynamic/adapters/service-directory-service.go +++ b/sources/gcp/dynamic/adapters/service-directory-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -22,17 +22,13 @@ var _ = registerableAdapter{ IAMPermissions: []string{"servicedirectory.services.get", "servicedirectory.services.list"}, PredefinedRole: "roles/servicedirectory.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Link from parent Service to child Endpoints via SEARCH // The framework will extract location, namespace, and service from the service name // and create a SEARCH query to find all endpoints under this service "name": { ToSDPItemType: gcpshared.ServiceDirectoryEndpoint, Description: "If the Service Directory Service is deleted or updated: All associated endpoints may become invalid or inaccessible. If an endpoint is updated: The service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, // Link to IP addresses in endpoint addresses (if endpoints are included in the response) diff --git a/sources/gcp/dynamic/adapters/service-usage-service.go b/sources/gcp/dynamic/adapters/service-usage-service.go index b07eefb6..350a9074 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service.go +++ b/sources/gcp/dynamic/adapters/service-usage-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -35,70 +35,44 @@ var _ = registerableAdapter{ IAMPermissions: []string{"serviceusage.services.get", "serviceusage.services.list"}, PredefinedRole: "roles/serviceusage.serviceUsageViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "parent": { ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the Project is deleted or updated: The Service Usage Service may become invalid or inaccessible. If the Service Usage Service is updated: The project remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "config.name": { ToSDPItemType: stdlib.NetworkDNS, Description: "The DNS address at which this service is available. They are tightly coupled with the Service Usage Service.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "config.usage.producerNotificationChannel": { // Google Service Management currently only supports Google Cloud Pub/Sub as a notification channel. // To use Google Cloud Pub/Sub as the channel, this must be the name of a Cloud Pub/Sub topic ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The Service Usage Service may fail to send notifications. If the Service Usage Service is updated: The topic remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "config.endpoints.name": { ToSDPItemType: stdlib.NetworkDNS, Description: "The canonical DNS name of the endpoint. DNS names and endpoints are tightly coupled - if DNS resolution fails, the endpoint becomes inaccessible.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "config.endpoints.target": { // The target field can contain either an IP address or FQDN. // The linker automatically detects which type the value is and creates the appropriate link. ToSDPItemType: stdlib.NetworkIP, Description: "The address of the API frontend (IP address or FQDN). Network connectivity to this address is required for the endpoint to function. The linker automatically detects whether the value is an IP address or DNS name.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "config.endpoints.aliases": { // Note: This field is deprecated but may still be present in existing configurations. // The linker will process each alias in the array. ToSDPItemType: stdlib.NetworkDNS, Description: "Additional DNS names/aliases for the endpoint. DNS names and endpoints are tightly coupled - if DNS resolution fails, the endpoint becomes inaccessible.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "config.documentation.documentationRootUrl": { ToSDPItemType: stdlib.NetworkHTTP, Description: "The HTTP/HTTPS URL to the root of the service documentation. HTTP connectivity to this URL is required to access the documentation.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "config.documentation.serviceRootUrl": { ToSDPItemType: stdlib.NetworkHTTP, Description: "The HTTP/HTTPS service root URL. HTTP connectivity to this URL may be required for service operations.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/service-usage-service_test.go b/sources/gcp/dynamic/adapters/service-usage-service_test.go index d9526938..51ced862 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service_test.go +++ b/sources/gcp/dynamic/adapters/service-usage-service_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/serviceusage/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -72,10 +72,6 @@ func TestServiceUsageService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serviceName, ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/spanner-backup.go b/sources/gcp/dynamic/adapters/spanner-backup.go index f42309f2..3dce3115 100644 --- a/sources/gcp/dynamic/adapters/spanner-backup.go +++ b/sources/gcp/dynamic/adapters/spanner-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -20,31 +20,27 @@ var _ = registerableAdapter{ UniqueAttributeKeys: []string{"instances", "backups"}, IAMPermissions: []string{"spanner.backups.get", "spanner.backups.list"}, }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // This is a backlink to instance. // Framework will extract the instance name and create the linked item query with GET "name": { Description: "If the Spanner Instance is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The instance remains unaffected.", ToSDPItemType: gcpshared.SpannerInstance, - BlastPropagation: gcpshared.ImpactInOnly, }, // Name of the database from which this backup is created. "database": { Description: "If the Spanner Database is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The database remains unaffected.", ToSDPItemType: gcpshared.SpannerDatabase, - BlastPropagation: gcpshared.ImpactInOnly, }, // Names of databases restored from this backup. May be across instances. "referencingDatabases": { Description: "If any of the databases restored from this backup are deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The restored databases remain unaffected.", ToSDPItemType: gcpshared.SpannerDatabase, - BlastPropagation: gcpshared.ImpactInOnly, }, // Names of destination backups copying this source backup. "referencingBackups": { Description: "If any of the destination backups copying this source backup are deleted or updated: The source backup may become invalid or inaccessible. If the source backup is updated: The destination backups remain unaffected.", ToSDPItemType: gcpshared.SpannerBackup, - BlastPropagation: gcpshared.ImpactInOnly, }, "encryptionInfo.kmsKeyVersion": gcpshared.CryptoKeyVersionImpactInOnly, // All Cloud KMS key versions used for encrypting the backup. @@ -53,13 +49,11 @@ var _ = registerableAdapter{ "backupSchedules": { Description: "If any of the backup schedules associated with this backup are deleted or updated: The Backup may stop being created automatically. If the Backup is updated: The backup schedules remain unaffected.", ToSDPItemType: gcpshared.SpannerBackupSchedule, - BlastPropagation: gcpshared.ImpactInOnly, }, // The instance partitions storing the backup (from the state at versionTime). "instancePartitions.instancePartition": { Description: "If any of the instance partitions storing this backup are deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The instance partitions remain unaffected.", ToSDPItemType: gcpshared.SpannerInstancePartition, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/spanner-database.go b/sources/gcp/dynamic/adapters/spanner-database.go index d2347a30..257e107f 100644 --- a/sources/gcp/dynamic/adapters/spanner-database.go +++ b/sources/gcp/dynamic/adapters/spanner-database.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -23,20 +23,18 @@ var _ = registerableAdapter{ IAMPermissions: []string{"spanner.databases.get", "spanner.databases.list"}, PredefinedRole: "overmind_custom_role", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The Cloud KMS key used to encrypt the database. "encryptionConfig.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "encryptionConfig.kmsKeyNames": gcpshared.CryptoKeyImpactInOnly, "restoreInfo.backupInfo.backup": { Description: "If the Spanner Backup is deleted or updated: The Database may become invalid or inaccessible. If the Database is updated: The backup remains unaffected.", ToSDPItemType: gcpshared.SpannerBackup, - BlastPropagation: gcpshared.ImpactInOnly, }, // Source database from which the backup was taken (if database was restored from backup). "restoreInfo.backupInfo.sourceDatabase": { Description: "If the source Database is deleted or updated: The restored Database may become invalid or lose its restore point reference. If the restored Database is updated: The source database remains unaffected.", ToSDPItemType: gcpshared.SpannerDatabase, - BlastPropagation: gcpshared.ImpactInOnly, }, "encryptionInfo.kmsKeyVersion": gcpshared.CryptoKeyVersionImpactInOnly, // This is a backlink to instance. @@ -48,7 +46,6 @@ var _ = registerableAdapter{ "name": { Description: "If the Spanner Instance is deleted or updated: The Database may become invalid or inaccessible. If the Database is updated: The instance remains unaffected.", ToSDPItemType: gcpshared.SpannerInstance, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/spanner-database_test.go b/sources/gcp/dynamic/adapters/spanner-database_test.go index ef14bb83..adc2e351 100644 --- a/sources/gcp/dynamic/adapters/spanner-database_test.go +++ b/sources/gcp/dynamic/adapters/spanner-database_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/spanner/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -87,40 +87,24 @@ func TestSpannerDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-instance", "my-backup"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "array-key-1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // name field creates a backlink to the Spanner instance @@ -128,10 +112,6 @@ func TestSpannerDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/spanner-instance-config.go b/sources/gcp/dynamic/adapters/spanner-instance-config.go index 8583b0f4..da9a01d4 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance-config.go +++ b/sources/gcp/dynamic/adapters/spanner-instance-config.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"spanner.instanceConfigs.get", "spanner.instanceConfigs.list"}, PredefinedRole: "roles/spanner.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{}, + linkRules: map[string]*gcpshared.Impact{}, terraformMapping: gcpshared.TerraformMapping{ Description: "There is no terraform resource for this type.", }, diff --git a/sources/gcp/dynamic/adapters/spanner-instance.go b/sources/gcp/dynamic/adapters/spanner-instance.go index f15c2de9..60780633 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance.go +++ b/sources/gcp/dynamic/adapters/spanner-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -22,24 +22,16 @@ var spannerInstanceAdapter = registerableAdapter{ //nolint:unused // HEALTH: https://cloud.google.com/spanner/docs/reference/rest/v1/projects.instances#State // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "config": { ToSDPItemType: gcpshared.SpannerInstanceConfig, Description: "If the Spanner Instance Config is deleted or updated: The Spanner Instance may fail to operate correctly. If the Spanner Instance is updated: The config remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // This is a link from parent to child via SEARCH // We need to make sure that the linked item supports `SEARCH` method for the `instance` name. "name": { ToSDPItemType: gcpshared.SpannerDatabase, Description: "If the Spanner Instance is deleted or updated: All associated databases may become invalid or inaccessible. If a database is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/spanner-instance_test.go b/sources/gcp/dynamic/adapters/spanner-instance_test.go index 8fe9fdf5..2f543c10 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance_test.go +++ b/sources/gcp/dynamic/adapters/spanner-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -124,19 +124,12 @@ func TestSpannerInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "regional-us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.SpannerDatabase.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: instanceName, - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - Out: true, - }, + ExpectedScope: projectID, }, } diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go index 345cb4ec..16d5aa06 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -24,14 +24,10 @@ var _ = registerableAdapter{ IAMPermissions: []string{"cloudsql.backupRuns.get", "cloudsql.backupRuns.list"}, PredefinedRole: "roles/cloudsql.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "instance": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "They are tightly coupled", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "diskEncryptionConfiguration.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // The Cloud KMS key version used to encrypt the backup. diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup.go b/sources/gcp/dynamic/adapters/sql-admin-backup.go index aac90d48..036a50ca 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -24,14 +24,10 @@ var _ = registerableAdapter{ IAMPermissions: []string{"cloudsql.backupRuns.get", "cloudsql.backupRuns.list"}, PredefinedRole: "roles/cloudsql.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "instance": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The instance cannot recover from the backup.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "kmsKey": gcpshared.CryptoKeyImpactInOnly, "kmsKeyVersion": gcpshared.CryptoKeyVersionImpactInOnly, @@ -45,10 +41,6 @@ var _ = registerableAdapter{ "instanceSettings.settings.ipConfiguration.allocatedIpRange": { ToSDPItemType: gcpshared.NetworkConnectivityInternalRange, Description: "If the Reserved Internal Range is deleted or updated: The backup's instance settings snapshot may reference an invalid IP range configuration. If the backup is updated: The internal range remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go index df1bdb2f..1ff3843b 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/sqladmin/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -90,10 +90,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // kmsKey @@ -101,10 +97,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // kmsKeyVersion @@ -112,10 +104,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // instanceSettings.settings.ipConfiguration.privateNetwork @@ -123,10 +111,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // instanceSettings.settings.ipConfiguration.authorizedNetworks.value (first entry) @@ -134,10 +118,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.0/24", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // instanceSettings.settings.ipConfiguration.authorizedNetworks.value (second entry) @@ -145,13 +125,9 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "198.51.100.5/32", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Note: allocatedIpRange link is not tested here because the NetworkConnectivityInternalRange adapter doesn't exist yet. - // The blast propagation is defined in the adapter so it will work automatically when the adapter is created. + // The link rule is defined in the adapter so it will work automatically when the adapter is created. } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance.go b/sources/gcp/dynamic/adapters/sql-admin-instance.go index 7c716d61..f0d003b7 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -31,7 +31,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/sql/docs/mysql/admin-api/rest/v1/instances#SqlInstanceState }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // VPC network used for private service connectivity. "settings.ipConfiguration.privateNetwork": gcpshared.ComputeNetworkImpactInOnly, // CMEK used to encrypt the primary data disk. @@ -42,25 +42,21 @@ var _ = registerableAdapter{ "settings.sqlServerAuditConfig.bucket": { Description: "If the Storage Bucket is deleted or updated: The Cloud SQL Instance may fail to write audit logs. If the Cloud SQL Instance is updated: The bucket remains unaffected.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Name of the primary (master) instance this replica depends on. "masterInstanceName": { Description: "If the master instance is deleted or updated: This replica may lose replication or become stale. If this replica is updated: The master remains unaffected.", ToSDPItemType: gcpshared.SQLAdminInstance, - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Failover replica for high availability; changes in the failover target can impact this instance's HA posture. "failoverReplica.name": { Description: "If the failover replica is deleted or updated: High availability for this instance may be reduced or fail. If this instance is updated: The failover replica remains unaffected.", ToSDPItemType: gcpshared.SQLAdminInstance, - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Read replicas sourced from this primary instance. Changes to this instance can impact replicas, but replica changes typically do not impact the primary. "replicaNames": { Description: "If this primary instance is deleted or materially updated: Its replicas may become unavailable or invalid. Changes on replicas generally do not impact the primary.", ToSDPItemType: gcpshared.SQLAdminInstance, - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Added: All assigned IP addresses (public or private). Treated as tightly coupled network identifiers. "ipAddresses.ipAddress": gcpshared.IPImpactBothWays, @@ -71,10 +67,6 @@ var _ = registerableAdapter{ "dnsName": { Description: "Tightly coupled with the Cloud SQL Instance endpoint.", ToSDPItemType: stdlib.NetworkDNS, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Authorized networks (CIDR ranges) allowed to connect to the instance. "settings.ipConfiguration.authorizedNetworks.value": gcpshared.IPImpactBothWays, @@ -82,21 +74,13 @@ var _ = registerableAdapter{ "settings.ipConfiguration.allocatedIpRange": { Description: "If the Subnetwork's secondary IP range is deleted or updated: The Cloud SQL Instance may fail to allocate private IP addresses. If the instance is updated: The subnetwork remains unaffected.", ToSDPItemType: gcpshared.ComputeSubnetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // CA pool resource name when using customer-managed CAs. // Format: projects/{project}/locations/{region}/caPools/{caPoolId} // TODO: Private CA resource type (PrivateCACAPool) does not exist yet. Uncomment when created. // "settings.ipConfiguration.serverCaPool": { // Description: "If the Private CA Pool is deleted or updated: The Cloud SQL Instance may fail to use customer-managed certificates. If the instance is updated: The CA pool remains unaffected.", - // ToSDPItemType: gcpshared.PrivateCACAPool, - // BlastPropagation: &sdp.BlastPropagation{ - // In: false, - // Out: true, - // }, + // ToSDPItemType: gcpshared.PrivateCACAPool, // }, // Forward link from parent to child via SEARCH // Link to all backup runs for this instance @@ -111,10 +95,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.SQLAdminBackupRun, Description: "If the Cloud SQL Instance is deleted or updated: All associated Backup Runs may become invalid or inaccessible. If a Backup Run is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go index c959170b..3491a795 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/sqladmin/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -111,7 +111,7 @@ func TestSQLAdminInstance(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // settings.ipConfiguration.privateNetwork @@ -120,10 +120,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // diskEncryptionConfiguration.kmsKeyName { @@ -131,10 +127,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // settings.sqlServerAuditConfig.bucket { @@ -142,10 +134,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "audit-logs-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // ipAddresses.ipAddress { @@ -153,10 +141,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.50", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // ipv6Address { @@ -164,10 +148,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:db8::1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // serviceAccountEmailAddress { @@ -175,10 +155,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // dnsName { @@ -186,10 +162,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-sql-instance.database.google.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // masterInstanceName { @@ -197,10 +169,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "master-instance", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // failoverReplica.name { @@ -208,10 +176,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "failover-replica", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // replicaNames[0] { @@ -219,10 +183,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "replica-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // replicaNames[1] { @@ -230,10 +190,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "replica-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // name (parent to child search) { @@ -241,10 +197,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/storage-bucket.go b/sources/gcp/dynamic/adapters/storage-bucket.go index 74c86772..798827cb 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket.go +++ b/sources/gcp/dynamic/adapters/storage-bucket.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -29,7 +29,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"storage.buckets.get", "storage.buckets.list"}, PredefinedRole: "roles/storage.bucketViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // A Cloud KMS key that will be used to encrypt objects written to this bucket if no encryption method is specified as part of the object write request. "encryption.defaultKmsKeyName": gcpshared.CryptoKeyImpactInOnly, // Name of the network. @@ -39,13 +39,18 @@ var _ = registerableAdapter{ "logging.logBucket": { ToSDPItemType: gcpshared.LoggingBucket, Description: "If the Logging Bucket is deleted or updated: The Storage Bucket may fail to write logs. If the Storage Bucket is updated: The Logging Bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, + }, + // Parent-to-child: bucket name links to this bucket's IAM policy (SEARCH returns one policy item). + "name": { + ToSDPItemType: gcpshared.StorageBucketIAMPolicy, + Description: "If the Storage Bucket is deleted or updated: Its IAM policy may become invalid. If the IAM policy is updated: The bucket remains unaffected.", + IsParentToChild: true, }, // TODO: Add parent-to-child links once the child adapters are implemented: // - StorageBucketAccessControl (requires adapter implementation) // - StorageDefaultObjectAccessControl (requires adapter implementation) // - StorageNotificationConfig (requires adapter implementation) - // Note: Parent-to-child links must use the "name" field (not array fields like "acl") + // Note: Only one parent-to-child link per field (map limitation). "name" is used for StorageBucketIAMPolicy. // since the linkItem function iterates into arrays before calling AutoLink, causing // keys like "acl.entity" instead of "acl" which would never match. // The framework only supports one parent-to-child link per field (map limitation). @@ -57,6 +62,26 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_storage_bucket.name", }, + // IAM resources for Storage Buckets. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent bucket for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket_iam + { + // Authoritative for a given role — grants the role to a list of members. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_binding.bucket", + }, + { + // Non-authoritative — grants a single member a single role. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_member.bucket", + }, + { + // Authoritative for the entire IAM policy on the bucket. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_policy.bucket", + }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/storage-bucket_test.go b/sources/gcp/dynamic/adapters/storage-bucket_test.go index 272ceece..937922be 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket_test.go +++ b/sources/gcp/dynamic/adapters/storage-bucket_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/storage/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -70,10 +70,13 @@ func TestStorageBucket(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, + }, + { + // name -> StorageBucketIAMPolicy (parent-to-child: one policy per bucket, GET by bucket name) + ExpectedType: gcpshared.StorageBucketIAMPolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: bucketName, + ExpectedScope: projectID, }, } diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go index e0ecb8e9..d13eeae9 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -35,21 +35,15 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631 status // https://cloud.google.com/storage-transfer/docs/reference/rest/v1/transferJobs#TransferJob.status }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Transfer spec references to source and destination storage "transferSpec.gcsDataSource.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the source GCS bucket is deleted or inaccessible: The transfer job will fail. If the transfer job is updated: The source bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "transferSpec.gcsDataSink.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the destination GCS bucket is deleted or inaccessible: The transfer job will fail. If the transfer job is updated: The destination bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: Investigate how we can link to AWS and Azure source when the account id (scope) is not available // https://cloud.google.com/storage-transfer/docs/reference/rest/v1/TransferSpec#AwsS3Data @@ -58,97 +52,59 @@ var _ = registerableAdapter{ "transferSpec.awsS3DataSource.credentialsSecret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager secret containing AWS credentials is deleted or updated: The transfer job may fail to authenticate with AWS S3. If the transfer job is updated: The secret remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // AWS S3 data source CloudFront domain (HTTP endpoint) "transferSpec.awsS3DataSource.cloudfrontDomain": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the CloudFront domain endpoint is unreachable: The transfer job will fail to access the source data via CloudFront. If the transfer job is updated: The CloudFront endpoint remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Azure Blob Storage data source credentials secret (Secret Manager) "transferSpec.azureBlobStorageDataSource.credentialsSecret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager secret containing Azure SAS token is deleted or updated: The transfer job may fail to authenticate with Azure Blob Storage. If the transfer job is updated: The secret remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Agent pool for POSIX source "transferSpec.sourceAgentPoolName": { ToSDPItemType: gcpshared.StorageTransferAgentPool, Description: "If the source Agent Pool is deleted or updated: The transfer job may fail to access POSIX source file systems. If the transfer job is updated: The agent pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Agent pool for POSIX sink "transferSpec.sinkAgentPoolName": { ToSDPItemType: gcpshared.StorageTransferAgentPool, Description: "If the sink Agent Pool is deleted or updated: The transfer job may fail to write to POSIX sink file systems. If the transfer job is updated: The agent pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Transfer manifest location (gs:// URI pointing to manifest file) "transferSpec.transferManifest.location": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket containing the transfer manifest is deleted or inaccessible: The transfer job may fail to read the manifest file. If the transfer job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // HTTP data source URL - link to HTTP endpoint using stdlib "transferSpec.httpDataSource.listUrl": { ToSDPItemType: stdlib.NetworkHTTP, Description: "HTTP data source URL for transfer operations. If the HTTP endpoint is unreachable: The transfer job will fail to access the source data.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "transferSpec.gcsIntermediateDataLocation.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the intermediate GCS bucket is deleted or inaccessible: The transfer job will fail. If the transfer job is updated: The intermediate bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Replication spec source bucket "replicationSpec.gcsDataSource.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the source GCS bucket for replication is deleted or inaccessible: The replication job will fail. If the replication job is updated: The source bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Replication spec destination bucket "replicationSpec.gcsDataSink.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the destination GCS bucket for replication is deleted or inaccessible: The replication job will fail. If the replication job is updated: The destination bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "serviceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "If the Service Account is deleted or permissions are revoked: The transfer job may fail to execute. If the transfer job is updated: The service account remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Notification configuration "notificationConfig.pubsubTopic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted: Transfer job notifications will fail. If the transfer job is updated: The Pub/Sub topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: Investigate whether we can/should support multiple items for a given key. // In this case, the eventStream can be an AWS SQS ARN in the form 'arn:aws:sqs:region:account_id:queue_name' @@ -158,18 +114,11 @@ var _ = registerableAdapter{ "eventStream.name": { ToSDPItemType: gcpshared.PubSubSubscription, Description: "If the Pub/Sub Subscription for event streaming is deleted: Transfer job events will not be consumed. If the transfer job is updated: The Pub/Sub subscription remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Latest transfer operation (child resource) "latestOperationName": { ToSDPItemType: gcpshared.StorageTransferTransferOperation, Description: "If the Transfer Operation is deleted or updated: The transfer job's latest operation reference may become invalid. If the transfer job is updated: The operation remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go index b8784fa6..b8f85d5c 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/storagetransfer/apiv1/storagetransferpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -120,10 +120,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // transferSpec.gcsDataSink.bucketName { @@ -131,10 +127,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "dest-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // serviceAccount { @@ -142,10 +134,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // notificationConfig.pubsubTopic { @@ -153,10 +141,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "transfer-notifications", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -190,10 +174,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/urllist.tsv", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // transferSpec.gcsDataSink.bucketName { @@ -201,10 +181,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "http-dest-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // transferSpec.gcsIntermediateDataLocation.bucketName { @@ -212,10 +188,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "intermediate-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // serviceAccount { @@ -223,10 +195,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa2@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // eventStream.name { @@ -234,10 +202,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "transfer-events", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters_test.go b/sources/gcp/dynamic/adapters_test.go index a09cedeb..59ba8c50 100644 --- a/sources/gcp/dynamic/adapters_test.go +++ b/sources/gcp/dynamic/adapters_test.go @@ -4,9 +4,9 @@ import ( "net/http" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/shared.go b/sources/gcp/dynamic/shared.go index ca7a0271..b6ea6204 100644 --- a/sources/gcp/dynamic/shared.go +++ b/sources/gcp/dynamic/shared.go @@ -3,6 +3,7 @@ package dynamic import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -13,10 +14,11 @@ import ( log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -53,6 +55,22 @@ var ( } ) +// enrichNOTFOUNDQueryError sets Scope, SourceName, ItemType, ResponderName on a NOTFOUND QueryError when they are empty, +// so cached/returned errors have consistent metadata for debugging and cache inspection. +func enrichNOTFOUNDQueryError(err error, scope, sourceName, itemType string) { + var qe *sdp.QueryError + if err == nil || !errors.As(err, &qe) || qe.GetErrorType() != sdp.QueryError_NOTFOUND { + return + } + if qe.GetScope() != "" { + return + } + qe.Scope = scope + qe.SourceName = sourceName + qe.ItemType = itemType + qe.ResponderName = sourceName +} + func linkItem(ctx context.Context, projectID string, sdpItem *sdp.Item, sdpAssetType shared.ItemType, linker *gcpshared.Linker, resp any, keys []string) { if value, ok := resp.(string); ok { linker.AutoLink(ctx, projectID, sdpItem, sdpAssetType, value, keys) @@ -144,12 +162,18 @@ func externalCallSingle(ctx context.Context, httpCli *http.Client, url string) ( defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err == nil { + body, readErr := io.ReadAll(resp.Body) + if resp.StatusCode == http.StatusNotFound { + // Return NOTFOUND regardless of body read so callers can cache via IsNotFound(err) + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("resource not found: %s", url), + } + } + if readErr == nil { if resp.StatusCode == http.StatusForbidden { return nil, &PermissionError{URL: url} } - return nil, fmt.Errorf( "failed to make a GET call: %s, HTTP Status: %s, HTTP Body: %s", url, @@ -157,12 +181,11 @@ func externalCallSingle(ctx context.Context, httpCli *http.Client, url string) ( string(body), ) } - log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.http.url": url, "ovm.source.http.response-status": resp.Status, - }).Warnf("failed to read the response body: %v", err) + }).Warnf("failed to read the response body: %v", readErr) return nil, fmt.Errorf("failed to make call: %s", resp.Status) } @@ -198,22 +221,27 @@ func externalCallMulti(ctx context.Context, itemsSelector string, httpCli *http. } if resp.StatusCode != http.StatusOK { - // Read the body to provide more context in the error message - body, err := io.ReadAll(resp.Body) - resp.Body.Close() // Close the response body - if err == nil { + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + // Return QueryError NOTFOUND so callers (streamSDPItems, aggregateSDPItems) can cache via IsNotFound(err) + return &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("resource not found: %s", currentURL), + } + } + if readErr == nil { return fmt.Errorf( "failed to make the GET call. HTTP Status: %s, HTTP Body: %s", resp.Status, string(body), ) } - log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.http.url-for-list": currentURL, "ovm.source.http.response-status": resp.Status, - }).Warnf("failed to read the response body: %v", err) + }).Warnf("failed to read the response body: %v", readErr) return fmt.Errorf("failed to make the GET call. HTTP Status: %s", resp.Status) } @@ -289,9 +317,9 @@ func externalCallMulti(ctx context.Context, itemsSelector string, httpCli *http. return nil } -func potentialLinksFromBlasts(itemType shared.ItemType, blasts map[shared.ItemType]map[string]*gcpshared.Impact) []string { +func potentialLinksFromLinkRules(itemType shared.ItemType, linkRules map[shared.ItemType]map[string]*gcpshared.Impact) []string { potentialLinksMap := make(map[string]bool) - for _, impact := range blasts[itemType] { + for _, impact := range linkRules[itemType] { potentialLinksMap[impact.ToSDPItemType.String()] = true // Special case: stdlib.NetworkIP and stdlib.NetworkDNS are interchangeable // because the linker automatically detects whether a value is an IP address or DNS name @@ -334,10 +362,15 @@ func aggregateSDPItems(ctx context.Context, a Adapter, url string, location gcps }, ) + hadExtractError := false + var lastExtractErr error for resp := range out { item, err := externalToSDP(ctx, location, a.uniqueAttributeKeys, resp, a.sdpAssetType, a.linker, a.nameSelector) if err != nil { log.WithError(err).Warn("failed to extract item from response") + hadExtractError = true + lastExtractErr = err + continue } items = append(items, item) @@ -345,15 +378,28 @@ func aggregateSDPItems(ctx context.Context, a Adapter, url string, location gcps err := p.Wait() if err != nil { + // If we have items but the pool failed with NOTFOUND (e.g. 404 on a later pagination page), + // return the items we collected so the caller does not cache NOTFOUND for a non-empty result. + if sources.IsNotFound(err) && len(items) > 0 { + return items, nil + } return nil, err } + // If all items failed extraction, return error so caller does not cache NOTFOUND (matches streamSDPItems) + if len(items) == 0 && hadExtractError && lastExtractErr != nil { + return nil, lastExtractErr + } + return items, nil } // streamSDPItems retrieves items from an external API and streams them as SDP items. func streamSDPItems(ctx context.Context, a Adapter, url string, location gcpshared.LocationInfo, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey) { itemsSelector := a.uniqueAttributeKeys[len(a.uniqueAttributeKeys)-1] // Use the last key as the item selector + if a.listResponseSelector != "" { + itemsSelector = a.listResponseSelector + } out := make(chan map[string]interface{}) p := pool.New().WithErrors().WithContext(ctx) @@ -367,10 +413,12 @@ func streamSDPItems(ctx context.Context, a Adapter, url string, location gcpshar }) itemsSent := 0 + hadExtractError := false for resp := range out { item, err := externalToSDP(ctx, location, a.uniqueAttributeKeys, resp, a.sdpAssetType, a.linker, a.nameSelector) if err != nil { log.WithError(err).Warn("failed to extract item from response") + hadExtractError = true continue } @@ -382,8 +430,27 @@ func streamSDPItems(ctx context.Context, a Adapter, url string, location gcpshar err := p.Wait() if err != nil { - cache.StoreError(ctx, err, shared.DefaultCacheDuration, cacheKey) - stream.SendError(err) + // Only cache NOTFOUND when no items were sent. For NOTFOUND, don't send error on stream + // so behaviour matches cached path (0 items, no error). When items were already sent, + // also don't send NOTFOUND (consistent with aggregateSDPItems returning items, nil). + if sources.IsNotFound(err) && itemsSent == 0 { + cache.StoreError(ctx, err, shared.DefaultCacheDuration, cacheKey) + } + if !sources.IsNotFound(err) { + stream.SendError(err) + } + } else if itemsSent == 0 && !hadExtractError { + // Cache not-found when no items were sent AND no extraction errors occurred + // If we had extraction errors, items may exist but couldn't be processed + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found in scope %s", a.sdpAssetType.String(), location.ToScope()), + Scope: location.ToScope(), + SourceName: a.Name(), + ItemType: a.sdpAssetType.String(), + ResponderName: a.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } // Note: No items found is valid. The caller's defer done() will release pending work. } @@ -434,14 +501,18 @@ func terraformMappingViaSearch(ctx context.Context, a Adapter, query string, loc resp, err := externalCallSingle(ctx, a.httpCli, getURL) if err != nil { - cache.StoreError(ctx, err, shared.DefaultCacheDuration, cacheKey) + enrichNOTFOUNDQueryError(err, location.ToScope(), a.Name(), a.Type()) + if sources.IsNotFound(err) { + cache.StoreError(ctx, err, shared.DefaultCacheDuration, cacheKey) + // Return empty result, nil error so behaviour matches cached NOTFOUND (caller converts to [], nil) + return []*sdp.Item{}, nil + } return nil, err } item, err := externalToSDP(ctx, location, a.uniqueAttributeKeys, resp, a.sdpAssetType, a.linker, a.nameSelector) if err != nil { wrappedErr := fmt.Errorf("failed to convert response to SDP: %w", err) - cache.StoreError(ctx, wrappedErr, shared.DefaultCacheDuration, cacheKey) return nil, wrappedErr } diff --git a/sources/gcp/dynamic/shared_test.go b/sources/gcp/dynamic/shared_test.go index 57f65625..1c7e099d 100644 --- a/sources/gcp/dynamic/shared_test.go +++ b/sources/gcp/dynamic/shared_test.go @@ -2,13 +2,18 @@ package dynamic import ( "context" + "encoding/json" "fmt" + "net/http" + "net/http/httptest" "reflect" "testing" "google.golang.org/protobuf/types/known/structpb" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) @@ -249,3 +254,211 @@ func Test_searchDescription_WithCustomSearchDescription(t *testing.T) { t.Errorf("searchDescription() got = %v, want %v", got, customDesc) } } + +// TestStreamSDPItemsZeroItemsCachesNotFound verifies that when the API returns zero items, +// streamSDPItems caches NOTFOUND so a subsequent Lookup returns the cached error. +func TestStreamSDPItemsZeroItemsCachesNotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"instances": []any{}}) + })) + defer server.Close() + + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + location := gcpshared.NewProjectLocation("test-project") + scope := location.ToScope() + listMethod := sdp.QueryMethod_LIST + + a := Adapter{ + httpCli: server.Client(), + uniqueAttributeKeys: []string{"instances"}, + sdpAssetType: gcpshared.ComputeInstance, + linker: &gcpshared.Linker{}, + nameSelector: "name", + listResponseSelector: "", + } + stream := discovery.NewRecordingQueryResultStream() + ck := sdpcache.CacheKeyFromParts(a.Name(), listMethod, scope, a.Type(), "") + + streamSDPItems(ctx, a, server.URL, location, stream, cache, ck) + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, a.Name(), listMethod, scope, a.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit after streamSDPItems with zero items") + } + if qErr == nil { + t.Fatal("expected cached NOTFOUND error, got nil") + } + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND, got %v", qErr.GetErrorType()) + } +} + +// ListCachesNotFoundWithMemoryCache verifies that when List returns 0 items, NOTFOUND is cached +// and a second List returns 0 items from cache without calling the API again. +func TestListCachesNotFoundWithMemoryCache(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"instances": []any{}}) + })) + defer server.Close() + + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + location := gcpshared.NewProjectLocation("test-project") + scope := location.ToScope() + + listEndpointFunc := func(loc gcpshared.LocationInfo) (string, error) { + return server.URL, nil + } + config := &AdapterConfig{ + Locations: []gcpshared.LocationInfo{location}, + HTTPClient: server.Client(), + GetURLFunc: func(string, gcpshared.LocationInfo) string { return "" }, + SDPAssetType: gcpshared.ComputeInstance, + SDPAdapterCategory: sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + Linker: &gcpshared.Linker{}, + UniqueAttributeKeys: []string{"instances"}, + NameSelector: "name", + ListResponseSelector: "", + } + adapter := NewListableAdapter(listEndpointFunc, config, cache) + discAdapter := adapter.(discovery.Adapter) + + // Prove cache is empty before the first query + cacheHit, _, _, _, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if cacheHit { + t.Fatal("cache should be empty before first List") + } + + items, err := adapter.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first List: expected 0 items, got %d", len(items)) + } + + // the not found error should be cached + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List, got %v", qErr) + } + + items, err = adapter.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second List: expected 0 items, got %d", len(items)) + } +} + +// SearchCachesNotFoundWithMemoryCache verifies that when Search returns 0 items, NOTFOUND is cached +// and a second Search returns 0 items from cache without calling the API again. +func TestSearchCachesNotFoundWithMemoryCache(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"instances": []any{}}) + })) + defer server.Close() + + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + location := gcpshared.NewProjectLocation("test-project") + scope := location.ToScope() + query := "some-instance" + + searchEndpointFunc := func(q string, loc gcpshared.LocationInfo) string { + return server.URL + } + config := &AdapterConfig{ + Locations: []gcpshared.LocationInfo{location}, + HTTPClient: server.Client(), + GetURLFunc: func(string, gcpshared.LocationInfo) string { return "" }, + SDPAssetType: gcpshared.ComputeInstance, + SDPAdapterCategory: sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + Linker: &gcpshared.Linker{}, + UniqueAttributeKeys: []string{"instances"}, + NameSelector: "name", + ListResponseSelector: "", + } + adapter := NewSearchableAdapter(searchEndpointFunc, config, "search by instances", cache) + discAdapter := adapter.(discovery.Adapter) + + // Prove cache is empty before the first query + cacheHit, _, _, _, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if cacheHit { + t.Fatal("cache should be empty before first Search") + } + + items, err := adapter.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + // the not found error should be cached + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = adapter.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } +} + +// TestStreamSDPItemsExtractionErrorDoesNotCacheNotFound verifies that when the API returns +// items but extraction fails (e.g. missing required "name"), streamSDPItems does NOT cache NOTFOUND. +func TestStreamSDPItemsExtractionErrorDoesNotCacheNotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Item without "name" causes externalToSDP to return error (ReturnsErrorWhenNameMissing). + _ = json.NewEncoder(w).Encode(map[string]any{ + "instances": []any{ + map[string]any{"foo": "bar"}, + }, + }) + })) + defer server.Close() + + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + location := gcpshared.NewProjectLocation("test-project") + scope := location.ToScope() + listMethod := sdp.QueryMethod_LIST + + a := Adapter{ + httpCli: server.Client(), + uniqueAttributeKeys: []string{"instances"}, + sdpAssetType: gcpshared.ComputeInstance, + linker: &gcpshared.Linker{}, + nameSelector: "name", + listResponseSelector: "", + } + stream := discovery.NewRecordingQueryResultStream() + ck := sdpcache.CacheKeyFromParts(a.Name(), listMethod, scope, a.Type(), "") + + streamSDPItems(ctx, a, server.URL, location, stream, cache, ck) + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, a.Name(), listMethod, scope, a.Type(), "", false) + done() + if cacheHit && qErr != nil && qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + t.Error("extraction errors must not result in NOTFOUND being cached") + } +} diff --git a/sources/gcp/integration-tests/big-query-model_test.go b/sources/gcp/integration-tests/big-query-model_test.go index 7a942d1c..73362aa9 100644 --- a/sources/gcp/integration-tests/big-query-model_test.go +++ b/sources/gcp/integration-tests/big-query-model_test.go @@ -9,7 +9,7 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-address_test.go b/sources/gcp/integration-tests/compute-address_test.go index f46215d2..008b1c83 100644 --- a/sources/gcp/integration-tests/compute-address_test.go +++ b/sources/gcp/integration-tests/compute-address_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-autoscaler_test.go b/sources/gcp/integration-tests/compute-autoscaler_test.go index a9b23d49..8853286e 100644 --- a/sources/gcp/integration-tests/compute-autoscaler_test.go +++ b/sources/gcp/integration-tests/compute-autoscaler_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-disk_test.go b/sources/gcp/integration-tests/compute-disk_test.go index 7990bb3e..58997c1c 100644 --- a/sources/gcp/integration-tests/compute-disk_test.go +++ b/sources/gcp/integration-tests/compute-disk_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-forwarding-rule_test.go b/sources/gcp/integration-tests/compute-forwarding-rule_test.go index 9958fc8b..0b747dbe 100644 --- a/sources/gcp/integration-tests/compute-forwarding-rule_test.go +++ b/sources/gcp/integration-tests/compute-forwarding-rule_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-healthcheck_test.go b/sources/gcp/integration-tests/compute-healthcheck_test.go index 22e589e9..4e8638ab 100644 --- a/sources/gcp/integration-tests/compute-healthcheck_test.go +++ b/sources/gcp/integration-tests/compute-healthcheck_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-image_test.go b/sources/gcp/integration-tests/compute-image_test.go index 92362562..a58ef552 100644 --- a/sources/gcp/integration-tests/compute-image_test.go +++ b/sources/gcp/integration-tests/compute-image_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance-group-manager_test.go b/sources/gcp/integration-tests/compute-instance-group-manager_test.go index 077b4805..e286d301 100644 --- a/sources/gcp/integration-tests/compute-instance-group-manager_test.go +++ b/sources/gcp/integration-tests/compute-instance-group-manager_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance-group_test.go b/sources/gcp/integration-tests/compute-instance-group_test.go index 48dce062..8017775e 100644 --- a/sources/gcp/integration-tests/compute-instance-group_test.go +++ b/sources/gcp/integration-tests/compute-instance-group_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance_test.go b/sources/gcp/integration-tests/compute-instance_test.go index 427c40ea..23d4fb87 100644 --- a/sources/gcp/integration-tests/compute-instance_test.go +++ b/sources/gcp/integration-tests/compute-instance_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instant-snapshot_test.go b/sources/gcp/integration-tests/compute-instant-snapshot_test.go index d9d0a8c6..09de7f70 100644 --- a/sources/gcp/integration-tests/compute-instant-snapshot_test.go +++ b/sources/gcp/integration-tests/compute-instant-snapshot_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-machine-image_test.go b/sources/gcp/integration-tests/compute-machine-image_test.go index 8d63a80e..59f1ca2b 100644 --- a/sources/gcp/integration-tests/compute-machine-image_test.go +++ b/sources/gcp/integration-tests/compute-machine-image_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-network_test.go b/sources/gcp/integration-tests/compute-network_test.go index 1f6b7b23..8fd975cb 100644 --- a/sources/gcp/integration-tests/compute-network_test.go +++ b/sources/gcp/integration-tests/compute-network_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/compute-node-group_test.go b/sources/gcp/integration-tests/compute-node-group_test.go index 0ade92f9..63edb298 100644 --- a/sources/gcp/integration-tests/compute-node-group_test.go +++ b/sources/gcp/integration-tests/compute-node-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -204,11 +204,7 @@ func TestComputeNodeGroupIntegration(t *testing.T) { ExpectedType: gcpshared.ComputeNodeGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nodeTemplateName, - ExpectedScope: "*", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, + ExpectedScope: "*", }, } diff --git a/sources/gcp/integration-tests/compute-reservation_test.go b/sources/gcp/integration-tests/compute-reservation_test.go index a6b7e8a2..2046f4b2 100644 --- a/sources/gcp/integration-tests/compute-reservation_test.go +++ b/sources/gcp/integration-tests/compute-reservation_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-snapshot_test.go b/sources/gcp/integration-tests/compute-snapshot_test.go index 77d472d0..b6a73c28 100644 --- a/sources/gcp/integration-tests/compute-snapshot_test.go +++ b/sources/gcp/integration-tests/compute-snapshot_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-subnetwork_test.go b/sources/gcp/integration-tests/compute-subnetwork_test.go index 4cc4d84b..f154ae4e 100644 --- a/sources/gcp/integration-tests/compute-subnetwork_test.go +++ b/sources/gcp/integration-tests/compute-subnetwork_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/computer-instance-template_test.go b/sources/gcp/integration-tests/computer-instance-template_test.go index 5e99a474..3e257c35 100644 --- a/sources/gcp/integration-tests/computer-instance-template_test.go +++ b/sources/gcp/integration-tests/computer-instance-template_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/service-account-impersonation_test.go b/sources/gcp/integration-tests/service-account-impersonation_test.go index cd4c93e5..53b380d8 100644 --- a/sources/gcp/integration-tests/service-account-impersonation_test.go +++ b/sources/gcp/integration-tests/service-account-impersonation_test.go @@ -13,12 +13,13 @@ import ( compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" + authcredentials "cloud.google.com/go/auth/credentials" + "cloud.google.com/go/auth/oauth2adapt" credentials "cloud.google.com/go/iam/credentials/apiv1" credentialspb "cloud.google.com/go/iam/credentials/apiv1/credentialspb" "github.com/google/uuid" "github.com/googleapis/gax-go/v2/apierror" "golang.org/x/oauth2" - "golang.org/x/oauth2/google" cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/googleapi" "google.golang.org/api/iam/v1" @@ -49,7 +50,8 @@ func TestServiceAccountImpersonationIntegration(t *testing.T) { // Initialize Cloud Resource Manager service crmService, err := cloudresourcemanager.NewService(t.Context()) if err != nil { - t.Fatalf("Failed to create Cloud Resource Manager service: %v", err) + t.Errorf("Failed to create Cloud Resource Manager service: %v", err) + return } state := &testState{ @@ -59,7 +61,8 @@ func TestServiceAccountImpersonationIntegration(t *testing.T) { // Initialize IAM service using Application Default Credentials iamService, err := iam.NewService(t.Context()) if err != nil { - t.Fatalf("Failed to create IAM service: %v", err) + t.Errorf("Failed to create IAM service: %v", err) + return } // Create UUIDs for service account names @@ -73,7 +76,9 @@ func TestServiceAccountImpersonationIntegration(t *testing.T) { // since this test needs to keep state between tests, we wrap it in a Run function t.Run("Run", func(t *testing.T) { - setupTest(t, t.Context(), iamService, crmService, state) + if !setupTest(t, t.Context(), iamService, crmService, state) { + return + } t.Cleanup(func() { teardownTest(t, t.Context(), iamService, crmService, state) @@ -94,12 +99,13 @@ func TestServiceAccountImpersonationIntegration(t *testing.T) { }) } -func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmService *cloudresourcemanager.Service, state *testState) { +func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmService *cloudresourcemanager.Service, state *testState) bool { // Create "Our Service Account" t.Logf("Creating 'Our Service Account': %s", state.ourServiceAccountID) ourSA, err := createServiceAccount(ctx, iamService, state.projectID, state.ourServiceAccountID, "Our Service Account for impersonation test") if err != nil { - t.Fatalf("Failed to create 'Our Service Account': %v", err) + t.Errorf("Failed to create 'Our Service Account': %v", err) + return false } state.ourServiceAccountEmail = ourSA.Email t.Logf("Created 'Our Service Account': %s", state.ourServiceAccountEmail) @@ -108,7 +114,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Logf("Creating 'Customer Service Account': %s", state.customerServiceAccountID) customerSA, err := createServiceAccount(ctx, iamService, state.projectID, state.customerServiceAccountID, "Customer Service Account for impersonation test") if err != nil { - t.Fatalf("Failed to create 'Customer Service Account': %v", err) + t.Errorf("Failed to create 'Customer Service Account': %v", err) + return false } state.customerServiceAccountEmail = customerSA.Email t.Logf("Created 'Customer Service Account': %s", state.customerServiceAccountEmail) @@ -136,7 +143,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe if attempt < maxAttempts { time.Sleep(1 * time.Second) } else { - t.Fatalf("Service account verification failed after %d attempts. The service accounts may not have been created correctly.", maxAttempts) + t.Errorf("Service account verification failed after %d attempts. The service accounts may not have been created correctly.", maxAttempts) + return false } } @@ -144,7 +152,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Logf("Granting impersonation permission to 'Our Service Account'") err = grantServiceAccountTokenCreator(ctx, iamService, state.projectID, state.customerServiceAccountEmail, state.ourServiceAccountEmail) if err != nil { - t.Fatalf("Failed to grant serviceAccountTokenCreator role: %v", err) + t.Errorf("Failed to grant serviceAccountTokenCreator role: %v", err) + return false } // Verify IAM policy binding is effective @@ -164,7 +173,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe if attempt < maxAttempts { time.Sleep(1 * time.Second) } else { - t.Fatalf("IAM policy binding verification failed after %d attempts. The role may not have been granted correctly.", maxAttempts) + t.Errorf("IAM policy binding verification failed after %d attempts. The role may not have been granted correctly.", maxAttempts) + return false } } @@ -172,7 +182,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Logf("Granting roles/compute.viewer to 'Customer Service Account' at project level") err = grantProjectIAMRole(ctx, crmService, state.projectID, state.customerServiceAccountEmail, "roles/compute.viewer") if err != nil { - t.Fatalf("Failed to grant roles/compute.viewer role: %v", err) + t.Errorf("Failed to grant roles/compute.viewer role: %v", err) + return false } // Create service account keys for authentication @@ -181,7 +192,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe // Create key for "Our Service Account" ourKey, err := createServiceAccountKey(ctx, iamService, state.projectID, state.ourServiceAccountEmail) if err != nil { - t.Fatalf("Failed to create key for 'Our Service Account': %v", err) + t.Errorf("Failed to create key for 'Our Service Account': %v", err) + return false } state.ourServiceAccountKey = []byte(ourKey.PrivateKeyData) state.ourServiceAccountKeyID = extractKeyID(ourKey.Name) @@ -190,7 +202,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe // Create key for "Customer Service Account" customerKey, err := createServiceAccountKey(ctx, iamService, state.projectID, state.customerServiceAccountEmail) if err != nil { - t.Fatalf("Failed to create key for 'Customer Service Account': %v", err) + t.Errorf("Failed to create key for 'Customer Service Account': %v", err) + return false } state.customerServiceAccountKey = []byte(customerKey.PrivateKeyData) state.customerServiceAccountKeyID = extractKeyID(customerKey.Name) @@ -201,21 +214,25 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Log("Verifying permission is actually effective by attempting GenerateAccessToken...") keyData, err := base64.StdEncoding.DecodeString(string(state.ourServiceAccountKey)) if err != nil { - t.Fatalf("Failed to decode service account key for verification: %v", err) + t.Errorf("Failed to decode service account key for verification: %v", err) + return false } maxAttempts = 60 // Allow more time for enforcement for attempt := 1; attempt <= maxAttempts; attempt++ { // Create credentials from "Our Service Account" key - testCreds, err := google.CredentialsFromJSON(ctx, keyData, iam.CloudPlatformScope) + testCreds, err := authcredentials.NewCredentialsFromJSON(authcredentials.ServiceAccount, keyData, &authcredentials.DetectOptions{Scopes: []string{iam.CloudPlatformScope}}) if err != nil { - t.Fatalf("Failed to create credentials for verification: %v", err) + t.Errorf("Failed to create credentials for verification: %v", err) + return false } + testTokenSource := oauth2adapt.TokenSourceFromTokenProvider(testCreds) // Create IAM Credentials client - testClient, err := credentials.NewIamCredentialsClient(ctx, option.WithTokenSource(testCreds.TokenSource)) + testClient, err := credentials.NewIamCredentialsClient(ctx, option.WithTokenSource(testTokenSource)) if err != nil { - t.Fatalf("Failed to create IAM Credentials client for verification: %v", err) + t.Errorf("Failed to create IAM Credentials client for verification: %v", err) + return false } // Attempt to generate a token to verify the permission is actually effective @@ -235,9 +252,11 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Logf("Attempt %d/%d: Permission not yet effective, error: %v, waiting...", attempt, maxAttempts, err) time.Sleep(2 * time.Second) } else { - t.Fatalf("Permission verification failed after %d attempts. The permission may not be enforced yet. Last error: %v", maxAttempts, err) + t.Errorf("Permission verification failed after %d attempts. The permission may not be enforced yet. Last error: %v", maxAttempts, err) + return false } } + return true } func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *testState) { @@ -246,20 +265,24 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t // Decode the service account key keyData, err := base64.StdEncoding.DecodeString(string(state.ourServiceAccountKey)) if err != nil { - t.Fatalf("Failed to decode service account key: %v", err) + t.Errorf("Failed to decode service account key: %v", err) + return } // Create credentials from the key - creds, err := google.CredentialsFromJSON(ctx, keyData, compute.DefaultAuthScopes()...) + creds, err := authcredentials.NewCredentialsFromJSON(authcredentials.ServiceAccount, keyData, &authcredentials.DetectOptions{Scopes: compute.DefaultAuthScopes()}) if err != nil { t.Logf("Key data: %s", string(keyData)) - t.Fatalf("Failed to create credentials from key: %v", err) + t.Errorf("Failed to create credentials from key: %v", err) + return } + tokenSource := oauth2adapt.TokenSourceFromTokenProvider(creds) // Create Compute Engine client using these credentials - client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(creds.TokenSource)) + client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(tokenSource)) if err != nil { - t.Fatalf("Failed to create Compute client: %v", err) + t.Errorf("Failed to create Compute client: %v", err) + return } defer client.Close() @@ -279,7 +302,8 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t // We expect a permission error if err == nil { - t.Fatal("Expected permission denied error, but listing succeeded") + t.Error("Expected permission denied error, but listing succeeded") + return } // Check if it's a permission error @@ -289,7 +313,8 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t t.Logf("✓ Correctly received permission denied error: %v", err) return } - t.Fatalf("Expected permission denied error, got: %v", err) + t.Errorf("Expected permission denied error, got: %v", err) + return } // Also check for googleapi.Error @@ -299,10 +324,11 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t t.Logf("✓ Correctly received permission denied error: %v", err) return } - t.Fatalf("Expected permission denied error, got: %v", err) + t.Errorf("Expected permission denied error, got: %v", err) + return } - t.Fatalf("Expected permission denied error, got unexpected error: %v", err) + t.Errorf("Expected permission denied error, got unexpected error: %v", err) } func testCustomerServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *testState) { @@ -311,19 +337,23 @@ func testCustomerServiceAccountDirectAuth(t *testing.T, ctx context.Context, sta // Decode the service account key keyData, err := base64.StdEncoding.DecodeString(string(state.customerServiceAccountKey)) if err != nil { - t.Fatalf("Failed to decode service account key: %v", err) + t.Errorf("Failed to decode service account key: %v", err) + return } // Create credentials from the key - creds, err := google.CredentialsFromJSON(ctx, keyData, compute.DefaultAuthScopes()...) + creds, err := authcredentials.NewCredentialsFromJSON(authcredentials.ServiceAccount, keyData, &authcredentials.DetectOptions{Scopes: compute.DefaultAuthScopes()}) if err != nil { - t.Fatalf("Failed to create credentials from key: %v", err) + t.Errorf("Failed to create credentials from key: %v", err) + return } + tokenSource := oauth2adapt.TokenSourceFromTokenProvider(creds) // Create Compute Engine client using these credentials - client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(creds.TokenSource)) + client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(tokenSource)) if err != nil { - t.Fatalf("Failed to create Compute client: %v", err) + t.Errorf("Failed to create Compute client: %v", err) + return } defer client.Close() @@ -341,7 +371,8 @@ func testCustomerServiceAccountDirectAuth(t *testing.T, ctx context.Context, sta it := client.List(ctx, req) _, err = it.Next() if err != nil { - t.Fatalf("Expected to successfully list instances, but got error: %v", err) + t.Errorf("Expected to successfully list instances, but got error: %v", err) + return } t.Log("✓ Successfully listed instances as 'Customer Service Account'") @@ -353,19 +384,23 @@ func testImpersonation(t *testing.T, ctx context.Context, state *testState) { // Decode the "Our Service Account" key keyData, err := base64.StdEncoding.DecodeString(string(state.ourServiceAccountKey)) if err != nil { - t.Fatalf("Failed to decode service account key: %v", err) + t.Errorf("Failed to decode service account key: %v", err) + return } // Create credentials from "Our Service Account" key - creds, err := google.CredentialsFromJSON(ctx, keyData, iam.CloudPlatformScope) + creds, err := authcredentials.NewCredentialsFromJSON(authcredentials.ServiceAccount, keyData, &authcredentials.DetectOptions{Scopes: []string{iam.CloudPlatformScope}}) if err != nil { - t.Fatalf("Failed to create credentials from key: %v", err) + t.Errorf("Failed to create credentials from key: %v", err) + return } + tokenSource := oauth2adapt.TokenSourceFromTokenProvider(creds) // Create IAM Credentials client using "Our Service Account" credentials - iamCredsClient, err := credentials.NewIamCredentialsClient(ctx, option.WithTokenSource(creds.TokenSource)) + iamCredsClient, err := credentials.NewIamCredentialsClient(ctx, option.WithTokenSource(tokenSource)) if err != nil { - t.Fatalf("Failed to create IAM Credentials client: %v", err) + t.Errorf("Failed to create IAM Credentials client: %v", err) + return } defer iamCredsClient.Close() @@ -377,17 +412,18 @@ func testImpersonation(t *testing.T, ctx context.Context, state *testState) { tokenResp, err := iamCredsClient.GenerateAccessToken(ctx, generateTokenReq) if err != nil { - t.Fatalf("Failed to generate access token for impersonated service account: %v", err) + t.Errorf("Failed to generate access token for impersonated service account: %v", err) + return } // Create Compute Engine client using the impersonated token - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{ + impersonatedTS := oauth2.StaticTokenSource(&oauth2.Token{ AccessToken: tokenResp.GetAccessToken(), }) - - client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(tokenSource)) + client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(impersonatedTS)) if err != nil { - t.Fatalf("Failed to create Compute client: %v", err) + t.Errorf("Failed to create Compute client: %v", err) + return } defer client.Close() @@ -405,7 +441,8 @@ func testImpersonation(t *testing.T, ctx context.Context, state *testState) { it := client.List(ctx, req) _, err = it.Next() if err != nil { - t.Fatalf("Expected to successfully list instances via impersonation, but got error: %v", err) + t.Errorf("Expected to successfully list instances via impersonation, but got error: %v", err) + return } t.Log("✓ Successfully listed instances via impersonation") diff --git a/sources/gcp/integration-tests/spanner-database_test.go b/sources/gcp/integration-tests/spanner-database_test.go index 6ca486bf..3500f1d2 100644 --- a/sources/gcp/integration-tests/spanner-database_test.go +++ b/sources/gcp/integration-tests/spanner-database_test.go @@ -14,7 +14,7 @@ import ( "github.com/googleapis/gax-go/v2/apierror" "google.golang.org/grpc/codes" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/integration-tests/spanner-instance_test.go b/sources/gcp/integration-tests/spanner-instance_test.go index 4d524f58..2ea00833 100644 --- a/sources/gcp/integration-tests/spanner-instance_test.go +++ b/sources/gcp/integration-tests/spanner-instance_test.go @@ -13,8 +13,8 @@ import ( log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc b/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc deleted file mode 100644 index 124a1be7..00000000 --- a/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc +++ /dev/null @@ -1,802 +0,0 @@ ---- -description: "GCP Manual Adapter development patterns and standards" -globs: **/*.go -alwaysApply: false ---- - -# GCP Manual Adapter Creation Rules - -This document provides comprehensive rules and patterns for creating GCP manual adapters in the Overmind platform. Follow these guidelines to ensure consistency, maintainability, and proper integration with the SDP (State Description Protocol) framework. - -## Table of Contents - -1. [Adapter Structure and Naming](#adapter-structure-and-naming) -2. [Wrapper Type Selection](#wrapper-type-selection) -3. [Base Struct Selection](#base-struct-selection) -4. [Required Methods Implementation](#required-methods-implementation) -5. [Terraform Mappings](#terraform-mappings) -6. [Get and Search Lookups](#get-and-search-lookups) -7. [Linked Item Queries](#linked-item-queries) -8. [Error Handling](#error-handling) -9. [Testing Patterns](#testing-patterns) -10. [Client Interface Patterns](#client-interface-patterns) -11. [Common Gotchas and Best Practices](#common-gotchas-and-best-practices) - -## Adapter Structure and Naming - -### File Naming Convention - -- Use kebab-case for file names: `compute-instance.go`, `big-query-table.go`, `cloud-kms-crypto-key.go` -- Test files should follow the same pattern with `_test.go` suffix: `compute-instance_test.go` - -### Package and Import Structure - -```go -package manual - -import ( - "context" - "errors" // if using iterator.Done - "fmt" // if using string formatting - "strings" // if parsing paths or URLs - - "cloud.google.com/go/compute/apiv1/computepb" // Use specific protobuf imports - "google.golang.org/api/iterator" // For handling GCP iterators - - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/sources" - gcpshared "github.com/overmindtech/cli/sources/gcp/shared" - "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sources/stdlib" // Only if linking to stdlib resources -) -``` - -### Struct Naming - -- Wrapper struct: `{resourceName}Wrapper` (e.g., `computeInstanceWrapper`, `bigQueryTableWrapper`) -- Use camelCase with first letter lowercase for private structs -- Constructor function: `New{ResourceName}` (e.g., `NewComputeInstance`, `NewBigQueryTable`) - -## Wrapper Type Selection - -Choose the appropriate wrapper interface based on GCP API capabilities: - -### `Wrapper` (GET only) - -Use when the GCP API only supports individual resource retrieval: - -- IAM Service Account Keys -- Compute Snapshots (individual retrieval) -- Resources without list/search endpoints - -### `ListableWrapper` (GET + LIST) - -Use when the GCP API supports listing all resources in a scope: - -- Compute Instances (per zone) -- Compute Disks (per zone) -- IAM Service Accounts (per project) -- BigQuery Datasets (per project) - -### `SearchableWrapper` (GET + SEARCH) - -Use when the GCP API supports filtering/searching resources: - -- BigQuery Tables (search within datasets) -- KMS Crypto Keys (search within key rings) -- KMS Key Rings (search within locations) - -### `SearchableListableWrapper` (GET + LIST + SEARCH) - -Use when the GCP API supports both listing and searching: - -- Currently not used in existing adapters, but available for complex resources - -## Base Struct Selection - -Choose the appropriate base struct based on GCP resource scope: - -### `ZoneBase` - Zonal Resources - -For resources scoped to a specific zone: - -```go -type computeInstanceWrapper struct { - client gcpshared.ComputeInstanceClient - *gcpshared.ZoneBase -} - -func NewComputeInstance(client gcpshared.ComputeInstanceClient, locations []gcpshared.LocationInfo) sources.ListableWrapper { - return &computeInstanceWrapper{ - client: client, - ZoneBase: gcpshared.NewZoneBase( - locations, - sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - gcpshared.ComputeInstance, - ), - } -} -``` - -**Examples:** Compute Instances, Compute Disks, Compute Snapshots, Compute Images - -### `RegionBase` - Regional Resources - -For resources scoped to a specific region: - -```go -type computeAddressWrapper struct { - client gcpshared.ComputeAddressClient - *gcpshared.RegionBase -} - -func NewComputeAddress(client gcpshared.ComputeAddressClient, projectID, region string) sources.ListableWrapper { - return &computeAddressWrapper{ - client: client, - RegionBase: gcpshared.NewRegionBase( - projectID, - region, - sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - gcpshared.ComputeAddress, - ), - } -} -``` - -**Examples:** Compute Addresses, Compute Reservations, Compute Resource Policies - -### `ProjectBase` - Project-Level Resources - -For resources scoped to the entire project: - -```go -type iamServiceAccountWrapper struct { - client gcpshared.IAMServiceAccountClient - *gcpshared.ProjectBase -} - -func NewIAMServiceAccount(client gcpshared.IAMServiceAccountClient, projectID string) sources.ListableWrapper { - return &iamServiceAccountWrapper{ - client: client, - ProjectBase: gcpshared.NewProjectBase( - projectID, - sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, - gcpshared.IAMServiceAccount, - ), - } -} -``` - -**Examples:** IAM Service Accounts, BigQuery Datasets, KMS Key Rings, Logging Sinks - -## Required Methods Implementation - -### IAM Permissions - -Always implement with specific GCP API permissions: - -```go -func (c computeInstanceWrapper) IAMPermissions() []string { - return []string{ - "compute.instances.get", - "compute.instances.list", - } -} -``` - -### Predefined Role - -Always implement with the most restrictive GCP predefined role: - -```go -func (c computeInstanceWrapper) PredefinedRole() string { - return "roles/compute.viewer" -} -``` - -### Potential Links - -Document all possible linked resources: - -```go -func (c computeInstanceWrapper) PotentialLinks() map[shared.ItemType]bool { - return shared.NewItemTypesSet( - stdlib.NetworkIP, - gcpshared.ComputeDisk, - gcpshared.ComputeSubnetwork, - gcpshared.ComputeNetwork, - gcpshared.ComputeResourcePolicy, - ) -} -``` - -## Terraform Mappings - -### GET Method (Direct ID Match) - -Use when Terraform resource has a unique identifier that directly matches your adapter's unique attribute: - -```go -func (c computeInstanceWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_GET, - // https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance#argument-reference - TerraformQueryMap: "google_compute_instance.name", - }, - } -} -``` - -### SEARCH Method (Multiple Parameters) - -Use when Terraform resource requires multiple parameters or different ID format: - -```go -func (b BigQueryTableWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_SEARCH, - // https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table - // Terraform uses: projects/{{project}}/datasets/{{dataset}}/tables/{{name}} - // Our adapter uses: dataset_id + table_id as separate parameters - TerraformQueryMap: "google_bigquery_table.id", - }, - } -} -``` - -### Multiple Terraform Mappings - -For resources that can be referenced in multiple ways: - -```go -func (c iamServiceAccountWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_GET, - TerraformQueryMap: "google_service_account.email", - }, - { - TerraformMethod: sdp.QueryMethod_GET, - TerraformQueryMap: "google_service_account.unique_id", - }, - } -} -``` - -## Get and Search Lookups - -### Single Key Lookup - -For resources with a single unique identifier: - -```go -var ComputeInstanceLookupByName = shared.NewItemTypeLookup("name", gcpshared.ComputeInstance) - -func (c computeInstanceWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - ComputeInstanceLookupByName, - } -} -``` - -### Multiple Keys Lookup (Order Matters) - -The order of lookups determines the order of `queryParts` in the `Get` method: - -```go -var ( - BigQueryDatasetLookupByID = shared.NewItemTypeLookup("id", gcpshared.BigQueryDataset) - BigQueryTableLookupByID = shared.NewItemTypeLookup("id", gcpshared.BigQueryTable) -) - -func (b BigQueryTableWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - BigQueryDatasetLookupByID, // First key: dataset ID (queryParts[0]) - BigQueryTableLookupByID, // Second key: table ID (queryParts[1]) - } -} - -func (b BigQueryTableWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - datasetID := queryParts[0] // From BigQueryDatasetLookupByID - tableID := queryParts[1] // From BigQueryTableLookupByID - // ... implementation -} -``` - -### Multiple Keys with Composite Lookup - -For complex hierarchical resources: - -```go -var ( - CloudKMSCryptoKeyRingLookupByLocation = shared.NewItemTypeLookup("location", gcpshared.CloudKMSKeyRing) - CloudKMSCryptoKeyRingLookupByName = shared.NewItemTypeLookup("name", gcpshared.CloudKMSKeyRing) - CloudKMSCryptoKeyLookupByName = shared.NewItemTypeLookup("name", gcpshared.CloudKMSCryptoKey) -) - -func (c cloudKMSCryptoKeyWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - CloudKMSCryptoKeyRingLookupByLocation, // First key: location - CloudKMSCryptoKeyRingLookupByName, // Second key: key ring name - CloudKMSCryptoKeyLookupByName, // Third key: crypto key name - } -} -``` - -### Search Lookups - -Define search parameters for SearchableWrapper: - -```go -// Single search lookup -func (b BigQueryTableWrapper) SearchLookups() []sources.ItemTypeLookups { - return []sources.ItemTypeLookups{ - { - BigQueryDatasetLookupByID, // Search within a specific dataset - }, - } -} - -// Multiple search lookups (different search patterns) -func (c cloudKMSCryptoKeyWrapper) SearchLookups() []sources.ItemTypeLookups { - return []sources.ItemTypeLookups{ - { - CloudKMSCryptoKeyRingLookupByLocation, // Search by location only - }, - { - CloudKMSCryptoKeyRingLookupByLocation, // Search within specific key ring - CloudKMSCryptoKeyRingLookupByName, - }, - } -} -``` - -## Linked Item Queries - -### Basic Pattern - -Always use this pattern for creating linked item queries: - -```go -sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.TargetResourceType.String(), - Method: sdp.QueryMethod_GET, // or SEARCH - Query: "resource-identifier", - Scope: "appropriate-scope", - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // This resource affected if linked resource changes - Out: true, // Linked resource affected if this resource changes - }, -}) -``` - -### Path Parameter Extraction - -Use helper functions for extracting parameters from GCP resource URLs: - -```go -// Extract parameters with their key names -// key: projects/my-project/locations/my-location/keyRings/my-key-ring/cryptoKeys/my-crypto-key -values := gcpshared.ExtractPathParams( - cryptoKey.GetName(), - "locations", "keyRings", "cryptoKeys" -) -``` - -### Composite Lookup Keys - -Use for multi-parameter queries: - -```go -Query: shared.CompositeLookupKey(location, keyRing, cryptoKey) -``` - -### Blast Propagation Rules - -- **In: true, Out: true** - Tightly coupled resources (parent-child relationships) -- **In: true, Out: false** - This resource depends on linked resource (e.g., disk depends on image) -- **In: false, Out: true** - Linked resource depends on this resource (e.g., instance depends on disk) - -### Common Linked Resource Patterns - -#### Network Resources - -```go -// Extract network name from full URL -if network := instance.GetNetwork(); network != "" { - if strings.Contains(network, "/") { - networkNameParts := strings.Split(network, "/") - networkName := networkNameParts[len(networkNameParts)-1] - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.ComputeNetwork.String(), - Method: sdp.QueryMethod_GET, - Query: networkName, - Scope: c.ProjectID(), // Networks are global - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } -} -``` - -#### KMS Resources - -```go -// Extract KMS key version parameters -if keyName := encryptionKey.GetKmsKeyName(); keyName != "" { - location := gcpshared.ExtractPathParam("locations", keyName) - keyRing := gcpshared.ExtractPathParam("keyRings", keyName) - cryptoKey := gcpshared.ExtractPathParam("cryptoKeys", keyName) - cryptoKeyVersion := gcpshared.ExtractPathParam("cryptoKeyVersions", keyName) - - if location != "" && keyRing != "" && cryptoKey != "" && cryptoKeyVersion != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSCryptoKeyVersion.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(location, keyRing, cryptoKey, cryptoKeyVersion), - Scope: c.ProjectID(), - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } -} -``` - -## Error Handling - -### Standard Error Pattern - -Always use source-specific error wrapping: - -```go -if err != nil { - return nil, gcpshared.QueryError(err, scope, c.Type()) -} -``` - -### Iterator Pattern - -Handle GCP API iterators consistently: - -```go -for { - item, err := it.Next() - if errors.Is(err, iterator.Done) { - break - } - if err != nil { - return nil, gcpshared.QueryError(err, scope, c.Type()) - } - // Process item... -} -``` - -### Stream Error Handling - -For streaming operations: - -```go -if err != nil { - stream.SendError(gcpshared.QueryError(err, scope, c.Type())) - return -} - -// For item processing errors, continue with next item -if sdpErr != nil { - stream.SendError(sdpErr) - continue -} -``` - -## Testing Patterns - -### Test Structure - -Follow this exact pattern for all adapter tests: - -```go -func TestComputeInstance(t *testing.T) { - ctx := context.Background() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := mocks.NewMockComputeInstanceClient(ctrl) - projectID := "test-project-id" - zone := "us-central1-a" - - t.Run("Get", func(t *testing.T) { - wrapper := manual.NewComputeInstance(mockClient, projectID, zone) - mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createComputeInstance("test-instance", computepb.Instance_RUNNING), nil) - - adapter := sources.WrapperToAdapter(wrapper) - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-instance", true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - { - ExpectedType: gcpshared.ComputeDisk.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-instance", - ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - // ... more test cases - } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) - }) - - t.Run("List", func(t *testing.T) { - // Test list functionality - }) - - t.Run("Search", func(t *testing.T) { - // Test search functionality (if applicable) - }) -} -``` - -### Required Test Cases - -- **Get method**: Test successful retrieval and error cases -- **StaticTests**: Test linked item queries using `shared.RunStaticTests` -- **HealthCheck**: Test health status mapping (if applicable) -- **List method**: Test the List method (for ListableWrapper) -- **Search method**: Test the Search method (for SearchableWrapper) -- **Interface compliance**: Verify adapter implements correct interfaces - -### Test Helper Functions - -Create helper functions for test data: - -```go -func createComputeInstance(instanceName string, status computepb.Instance_Status) *computepb.Instance { - return &computepb.Instance{ - Name: ptr.To(instanceName), - Labels: map[string]string{"env": "test"}, - Status: ptr.To(status.String()), - // ... other fields - } -} -``` - -### Mock Expectations - -Use gomock for client mocking: - -```go -mockClient := mocks.NewMockComputeInstanceClient(ctrl) -mockClient.EXPECT().Get(ctx, gomock.Any()).Return(expectedResult, nil) -``` - -## Client Interface Patterns - -### Typed Client Interfaces - -Define typed client interfaces in the shared package: - -```go -// In sources/gcp/shared/compute-clients.go -type ComputeInstanceClient interface { - Get(ctx context.Context, req *computepb.GetInstanceRequest, opts ...gax.CallOption) (*computepb.Instance, error) - List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) *compute.InstanceIterator -} - -// Constructor function -func NewComputeInstanceClient(client *compute.InstancesClient) ComputeInstanceClient { - return &computeInstanceClientImpl{client: client} -} -``` - -### Client Implementation - -Implement the interface with proper error handling: - -```go -type computeInstanceClientImpl struct { - client *compute.InstancesClient -} - -func (c *computeInstanceClientImpl) Get(ctx context.Context, req *computepb.GetInstanceRequest, opts ...gax.CallOption) (*computepb.Instance, error) { - return c.client.Get(ctx, req, opts...) -} - -func (c *computeInstanceClientImpl) List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) *compute.InstanceIterator { - return c.client.List(ctx, req, opts...) -} -``` - -## Common Gotchas and Best Practices - -### 1. Unique Attribute Consistency - -Ensure the `UniqueAttribute` in the SDP item matches the lookup key: - -```go -// If using composite lookup key -err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(values...)) -sdpItem := &sdp.Item{ - Type: gcpshared.CloudKMSCryptoKey.String(), - UniqueAttribute: "uniqueAttr", // Must match the attribute name above - // ... -} -``` - -### 2. Scope Handling - -Use appropriate scope helper functions: - -```go -// For zonal resources -scope := gcpshared.ZonalScope(projectID, zone) - -// For regional resources -scope := gcpshared.RegionalScope(projectID, region) - -// For project-level resources -scope := gcpshared.ProjectScope(projectID) -``` - -### 3. Attributes and Tags - -Use `ToAttributesWithExclude` to convert protobuf to attributes: - -```go -attributes, err := shared.ToAttributesWithExclude(instance, "labels") -tags := instance.GetLabels() // Use labels as tags -``` - -### 4. Health Status Mapping - -Map GCP resource statuses to SDP health statuses: - -```go -switch disk.GetStatus() { -case computepb.Disk_UNDEFINED_STATUS.String(): - sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() -case computepb.Disk_CREATING.String(), computepb.Disk_RESTORING.String(): - sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() -case computepb.Disk_FAILED.String(), computepb.Disk_UNAVAILABLE.String(): - sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() -case computepb.Disk_READY.String(): - sdpItem.Health = sdp.Health_HEALTH_OK.Enum() -} -``` - -### 5. Multiple Query Parameters - -For resources requiring multiple parameters: - -```go -func (b BigQueryTableWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - datasetID := queryParts[0] // First parameter - tableID := queryParts[1] // Second parameter - // ... implementation -} -``` - -### 6. Conditional Linked Queries - -Only create linked queries when the referenced resource exists: - -```go -if metadata.EncryptionConfig != nil && metadata.EncryptionConfig.KMSKeyName != "" { - // Create KMS key linked query -} -``` - -### 7. Path Validation - -Always validate extracted path parameters: - -```go -values := gcpshared.ExtractPathParams(keyName, "locations", "keyRings", "cryptoKeys") -if len(values) == 3 && values[0] != "" && values[1] != "" && values[2] != "" { - // Use the extracted values -} -``` - -### 8. Adapter Registration - -Register adapters in the main adapters file with proper scoping: - -```go -// Register zonal adapters -for _, zone := range zones { - adapters = append(adapters, - sources.WrapperToAdapter(NewComputeInstance( - shared.NewComputeInstanceClient(instanceCli), - projectID, - zone, - )), - ) -} - -// Register project-level adapters -adapters = append(adapters, - sources.WrapperToAdapter(NewIAMServiceAccount( - shared.NewIAMServiceAccountClient(iamCli), - projectID, - )), -) -``` - -### 9. Documentation Comments - -Always include GCP API documentation URLs in comments: - -```go -// The resource URL for the disk type associated with this disk. -// GET https://compute.googleapis.com/compute/v1/projects/{project}/zones/{zone}/diskTypes/{diskType} -// https://cloud.google.com/compute/docs/reference/rest/v1/diskTypes/get -``` - -### 10. Error Message Consistency - -Use consistent error messages and include context: - -```go -return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("invalid CryptoKey name: %s", cryptoKey.GetName()), -} -``` - -## Validation Checklist - -Before submitting a new adapter, ensure: - -- [ ] File follows naming convention (`{resource-name}.go`) -- [ ] Imports are properly organized and minimal -- [ ] Wrapper type matches GCP API capabilities -- [ ] Base struct matches resource scope (Zone/Region/Project) -- [ ] All required methods implemented (IAMPermissions, PredefinedRole, PotentialLinks) -- [ ] Terraform mappings are correct and include documentation URLs -- [ ] Get/Search lookups match the resource's query parameters -- [ ] Linked item queries use proper blast propagation -- [ ] Error handling uses `gcpshared.QueryError` -- [ ] Iterator pattern is correctly implemented -- [ ] Health status mapping is implemented (if applicable) -- [ ] Test file covers all required test cases -- [ ] Static tests validate linked item queries -- [ ] Client interface is properly defined in shared package -- [ ] Adapter is registered in the main adapters file -- [ ] Documentation comments include GCP API URLs -- [ ] Unique attribute consistency is maintained -- [ ] Scope handling uses appropriate helper functions -- [ ] Path parameter extraction is validated -- [ ] Conditional linked queries are properly implemented - -## Examples Reference - -For complete examples, refer to these existing adapters: - -- **Simple ListableWrapper**: `sources/gcp/manual/compute-instance.go` -- **Complex ListableWrapper**: `sources/gcp/manual/compute-disk.go` -- **SearchableWrapper with Multiple Keys**: `sources/gcp/manual/big-query-table.go` -- **SearchableWrapper with Composite Lookup**: `sources/gcp/manual/cloud-kms-crypto-key.go` -- **Project-level Resource**: `sources/gcp/manual/iam-service-account.go` -- **Regional Resource**: `sources/gcp/manual/compute-address.go` -- **Complex Linked Queries**: `sources/gcp/manual/compute-backend-service.go` - -These examples demonstrate all the patterns and best practices outlined in this document. diff --git a/sources/gcp/manual/README.md b/sources/gcp/manual/README.md index 8b4f9aac..dc2e8b09 100644 --- a/sources/gcp/manual/README.md +++ b/sources/gcp/manual/README.md @@ -114,10 +114,6 @@ t.Run("StaticTests", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-dataset", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // ... more test cases } diff --git a/sources/gcp/manual/adapters.go b/sources/gcp/manual/adapters.go index c43e3f21..9af03d1b 100644 --- a/sources/gcp/manual/adapters.go +++ b/sources/gcp/manual/adapters.go @@ -5,14 +5,16 @@ import ( "fmt" "cloud.google.com/go/bigquery" + certificatemanager "cloud.google.com/go/certificatemanager/apiv1" compute "cloud.google.com/go/compute/apiv1" iamAdmin "cloud.google.com/go/iam/admin/apiv1" logging "cloud.google.com/go/logging/apiv2" + "cloud.google.com/go/storage" "golang.org/x/oauth2" "google.golang.org/api/option" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -39,15 +41,17 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati instanceGroupManagerCli *compute.InstanceGroupManagersClient regionInstanceGroupManagerCli *compute.RegionInstanceGroupManagersClient diskCli *compute.DisksClient - iamServiceAccountKeyCli *iamAdmin.IamClient - iamServiceAccountCli *iamAdmin.IamClient - kmsLoader *shared.CloudKMSAssetLoader - bigQueryDatasetCli *bigquery.Client - loggingConfigCli *logging.ConfigClient + iamServiceAccountKeyCli *iamAdmin.IamClient + iamServiceAccountCli *iamAdmin.IamClient + certificateManagerCli *certificatemanager.Client + kmsLoader *shared.CloudKMSAssetLoader + bigQueryDatasetCli *bigquery.Client + loggingConfigCli *logging.ConfigClient nodeGroupCli *compute.NodeGroupsClient nodeTemplateCli *compute.NodeTemplatesClient regionBackendServiceCli *compute.RegionBackendServicesClient regionHealthCheckCli *compute.RegionHealthChecksClient + storageCli *storage.Client ) if initGCPClients { @@ -147,6 +151,12 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati return nil, fmt.Errorf("failed to create IAM service account client: %w", err) } + // Certificate Manager + certificateManagerCli, err = certificatemanager.NewClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("failed to create certificate manager client: %w", err) + } + // Extract project ID from projectLocations for BigQuery client initialization. // // IMPORTANT: The project ID passed to bigquery.NewClient() is used for: @@ -214,6 +224,11 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati if err != nil { return nil, fmt.Errorf("failed to create compute region health checks client: %w", err) } + + storageCli, err = storage.NewClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("failed to create storage client: %w", err) + } } var adapters []discovery.Adapter @@ -275,12 +290,15 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati sources.WrapperToAdapter(NewComputeSnapshot(shared.NewComputeSnapshotsClient(computeSnapshotCli), projectLocations), cache), sources.WrapperToAdapter(NewIAMServiceAccountKey(shared.NewIAMServiceAccountKeyClient(iamServiceAccountKeyCli), projectLocations), cache), sources.WrapperToAdapter(NewIAMServiceAccount(shared.NewIAMServiceAccountClient(iamServiceAccountCli), projectLocations), cache), + sources.WrapperToAdapter(NewCertificateManagerCertificate(shared.NewCertificateManagerCertificateClient(certificateManagerCli), projectLocations), cache), sources.WrapperToAdapter(NewCloudKMSKeyRing(kmsLoader, projectLocations), cache), sources.WrapperToAdapter(NewCloudKMSCryptoKey(kmsLoader, projectLocations), cache), sources.WrapperToAdapter(NewCloudKMSCryptoKeyVersion(kmsLoader, projectLocations), cache), sources.WrapperToAdapter(NewBigQueryDataset(shared.NewBigQueryDatasetClient(bigQueryDatasetCli), projectLocations), cache), + sources.WrapperToAdapter(NewBigQueryTable(shared.NewBigQueryTableClient(bigQueryDatasetCli), projectLocations), cache), sources.WrapperToAdapter(NewLoggingSink(shared.NewLoggingConfigClient(loggingConfigCli), projectLocations), cache), sources.WrapperToAdapter(NewBigQueryRoutine(shared.NewBigQueryRoutineClient(bigQueryDatasetCli), projectLocations), cache), + sources.WrapperToAdapter(NewStorageBucketIAMPolicy(shared.NewStorageBucketIAMPolicyGetter(storageCli), projectLocations), cache), ) } diff --git a/sources/gcp/manual/big-query-dataset.go b/sources/gcp/manual/big-query-dataset.go index 4b435b6c..0b9175b5 100644 --- a/sources/gcp/manual/big-query-dataset.go +++ b/sources/gcp/manual/big-query-dataset.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -62,6 +62,23 @@ func (b BigQueryDatasetWrapper) TerraformMappings() []*sdp.TerraformMapping { TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_bigquery_dataset.dataset_id", }, + // IAM resources for BigQuery Datasets. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent dataset for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_dataset_iam + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigquery_dataset_iam_binding.dataset_id", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigquery_dataset_iam_member.dataset_id", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigquery_dataset_iam_policy.dataset_id", + }, } } @@ -159,10 +176,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: parts[1], Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) // Link to contained tables. @@ -173,10 +186,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: parts[1], Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to contained routines. @@ -187,10 +196,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: parts[1], Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) for _, access := range metadata.Access { @@ -205,10 +210,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: access.Entity, Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -222,16 +223,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: access.Dataset.Dataset.DatasetID, Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - /* - A grant authorizing all resources of a particular type in a particular dataset access to this dataset. - Only views are supported for now. - The role field is not required when this field is set. - If that dataset is deleted and re-created, its access needs to be granted again via an update operation. - */ - In: false, - Out: true, - }, }) } } @@ -247,10 +238,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: shared.CompositeLookupKey(values...), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -267,10 +254,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: shared.CompositeLookupKey(values...), Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/big-query-dataset_test.go b/sources/gcp/manual/big-query-dataset_test.go index dcb28e15..8b137880 100644 --- a/sources/gcp/manual/big-query-dataset_test.go +++ b/sources/gcp/manual/big-query-dataset_test.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -45,70 +45,42 @@ func TestBigQueryDataset(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-user@example.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryDataset.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { ExpectedType: gcpshared.BigQueryModel.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryRoutine.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.BigQueryTable.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.BigQueryConnection.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-connection"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -151,6 +123,47 @@ func TestBigQueryDataset(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockBigQueryDatasetClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockClient.EXPECT().List(ctx, projectID, gomock.Any()).Return([]*sdp.Item{}, nil).Times(1) + + wrapper := manual.NewBigQueryDataset(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first List: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List, got %v", qErr) + } + + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second List: expected 0 items, got %d", len(items)) + } + }) } // createDataset creates a BigQuery Dataset for testing. diff --git a/sources/gcp/manual/big-query-model.go b/sources/gcp/manual/big-query-model.go index 936abbc8..7c722476 100644 --- a/sources/gcp/manual/big-query-model.go +++ b/sources/gcp/manual/big-query-model.go @@ -5,9 +5,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -91,10 +91,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc }, // Model is in a dataset, if dataset is deleted, model is deleted. // If the model is deleted, the dataset is not deleted. - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) if metadata.EncryptionConfig != nil && metadata.EncryptionConfig.KMSKeyName != "" { @@ -107,10 +103,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc Scope: location.ProjectID, Query: shared.CompositeLookupKey(values...), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -127,10 +119,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc Query: shared.CompositeLookupKey(dataSetId, row.DataSplitResult.EvaluationTable.TableId), }, // If the evaluation table is deleted or updated: The model's evaluation results may become invalid or inaccessible. If the model is updated: The table remains unaffected. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -144,10 +132,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc Query: shared.CompositeLookupKey(dataSetId, row.DataSplitResult.TrainingTable.TableId), }, // If the training table is deleted or updated: The model's training data may become invalid or inaccessible. If the model is updated: The table remains unaffected. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -161,10 +145,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc Query: shared.CompositeLookupKey(dataSetId, row.DataSplitResult.TestTable.TableId), }, // If the test table is deleted or updated: The model's test results may become invalid or inaccessible. If the model is updated: The table remains unaffected. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/big-query-model_test.go b/sources/gcp/manual/big-query-model_test.go index 5441a4a3..2a83d6ef 100644 --- a/sources/gcp/manual/big-query-model_test.go +++ b/sources/gcp/manual/big-query-model_test.go @@ -7,9 +7,9 @@ import ( bigquery "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -52,20 +52,12 @@ func TestBigQueryModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryDataset.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -100,6 +92,49 @@ func TestBigQueryModel(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockBigQueryModelClient(ctrl) + projectID := "cache-test-project" + scope := projectID + datasetID := "empty_dataset" + query := datasetID + + mockClient.EXPECT().List(ctx, projectID, datasetID, gomock.Any()).Return([]*sdp.Item{}, nil).Times(1) + + wrapper := manual.NewBigQueryModel(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, qErr := searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("first Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", cachedErr) + } + + items, qErr = searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("second Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("List_Unsupported", func(t *testing.T) { wrapper := manual.NewBigQueryModel(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/big-query-routine.go b/sources/gcp/manual/big-query-routine.go index 3548b49f..566645d9 100644 --- a/sources/gcp/manual/big-query-routine.go +++ b/sources/gcp/manual/big-query-routine.go @@ -5,9 +5,9 @@ import ( "strings" "cloud.google.com/go/bigquery" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -177,23 +177,15 @@ func (b BigQueryRoutineWrapper) gcpBigQueryRoutineToItem(metadata *bigquery.Rout Query: datasetID, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to imported libraries (GCS buckets) for JavaScript routines // Format: gs://bucket-name/path/to/file.js if len(metadata.ImportedLibraries) > 0 { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } if linkFunc, ok := gcpshared.ManualAdapterLinksByAssetType[gcpshared.StorageBucket]; ok { for _, libraryURI := range metadata.ImportedLibraries { if libraryURI != "" { - linkedQuery := linkFunc(location.ProjectID, location.ToScope(), libraryURI, blastPropagation) + linkedQuery := linkFunc(location.ProjectID, location.ToScope(), libraryURI) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) } @@ -232,11 +224,6 @@ func (b BigQueryRoutineWrapper) gcpBigQueryRoutineToItem(metadata *bigquery.Rout Query: shared.CompositeLookupKey(location, connectionID), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // If the Connection is deleted or updated: The remote function may fail to authenticate. If the routine is updated: The connection remains unaffected. - In: true, - Out: false, - }, }) } } @@ -253,11 +240,6 @@ func (b BigQueryRoutineWrapper) gcpBigQueryRoutineToItem(metadata *bigquery.Rout Query: endpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // If the HTTP endpoint is unreachable: The remote function will fail to execute. If the routine is updated: The endpoint remains unaffected. - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/big-query-routine_test.go b/sources/gcp/manual/big-query-routine_test.go index 8aa325c8..911e8609 100644 --- a/sources/gcp/manual/big-query-routine_test.go +++ b/sources/gcp/manual/big-query-routine_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -53,10 +53,6 @@ func TestBigQueryRoutine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Imported library GCS bucket link { @@ -64,10 +60,6 @@ func TestBigQueryRoutine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Remote function connection link { @@ -75,10 +67,6 @@ func TestBigQueryRoutine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us|example-conn", ExpectedScope: "example", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Remote function HTTP endpoint link { @@ -86,10 +74,6 @@ func TestBigQueryRoutine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/run", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -184,6 +168,49 @@ func TestBigQueryRoutine(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockBigQueryRoutineClient(ctrl) + projectID := "cache-test-project" + scope := projectID + datasetID := "empty_dataset" + query := datasetID + + mockClient.EXPECT().List(gomock.Any(), projectID, datasetID, gomock.Any()).Return([]*sdp.Item{}, nil).Times(1) + + wrapper := manual.NewBigQueryRoutine(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search with terraform format", func(t *testing.T) { wrapper := manual.NewBigQueryRoutine(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/big-query-table.go b/sources/gcp/manual/big-query-table.go index 1f9f9475..7f11a2e3 100644 --- a/sources/gcp/manual/big-query-table.go +++ b/sources/gcp/manual/big-query-table.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -57,7 +57,7 @@ func (b BigQueryTableWrapper) PotentialLinks() map[shared.ItemType]bool { ) } -// TerraformMappings returns the Terraform mappings for the BigQuery dataset wrapper +// TerraformMappings returns the Terraform mappings for the BigQuery table wrapper func (b BigQueryTableWrapper) TerraformMappings() []*sdp.TerraformMapping { return []*sdp.TerraformMapping{ { @@ -68,6 +68,25 @@ func (b BigQueryTableWrapper) TerraformMappings() []*sdp.TerraformMapping { // them to GET operations by extracting the last N path parameters (based on GetLookups count). TerraformQueryMap: "google_bigquery_table.id", }, + // IAM resources for BigQuery Tables. These are Terraform-only constructs + // (no standalone GCP API resource exists). We use the dataset_id attribute + // because table_id is a bare name that the SEARCH handler would misinterpret + // as a dataset ID. Using dataset_id lists all tables in the affected dataset, + // providing dataset-level blast radius for table IAM changes. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table_iam + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigquery_table_iam_binding.dataset_id", + }, + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigquery_table_iam_member.dataset_id", + }, + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigquery_table_iam_policy.dataset_id", + }, } } @@ -177,11 +196,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: parts[0], // dataset ID Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled. - In: true, - Out: true, - }, }) if metadata.EncryptionConfig != nil && metadata.EncryptionConfig.KMSKeyName != "" { @@ -197,11 +211,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: shared.CompositeLookupKey(values...), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled. - In: true, - Out: false, - }, }) } } @@ -239,11 +248,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: shared.CompositeLookupKey(connectionLocation, connectionID), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled. - In: true, - Out: true, - }, }) } } @@ -260,11 +264,7 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location // Use the StorageBucket linker to extract bucket name from various URI formats if linkFunc, ok := gcpshared.ManualAdapterLinksByAssetType[gcpshared.StorageBucket]; ok { // The linker handles gs:// URIs and extracts bucket names - linkedQuery := linkFunc(location.ProjectID, location.ToScope(), sourceURI, &sdp.BlastPropagation{ - // If the Storage Bucket is deleted or updated: The external table may fail to read data. If the table is updated: The bucket remains unaffected. - In: true, - Out: false, - }) + linkedQuery := linkFunc(location.ProjectID, location.ToScope(), sourceURI) if linkedQuery != nil { // Create a unique key from query and scope to deduplicate bucketKey := fmt.Sprintf("%s|%s", linkedQuery.GetQuery().GetQuery(), linkedQuery.GetQuery().GetScope()) @@ -293,11 +293,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: shared.CompositeLookupKey(baseTableRef.DatasetID, baseTableRef.TableID), Scope: baseTableRef.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // If the base table is deleted or updated: The snapshot may become invalid or inaccessible. If the snapshot is updated: The base table remains unaffected. - In: true, - Out: false, - }, }) } } @@ -316,11 +311,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: shared.CompositeLookupKey(baseTableRef.DatasetID, baseTableRef.TableID), Scope: baseTableRef.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // If the base table is deleted or updated: The clone may become invalid or inaccessible. If the clone is updated: The base table remains unaffected. - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/big-query-table_test.go b/sources/gcp/manual/big-query-table_test.go index 19bb8809..22a261f2 100644 --- a/sources/gcp/manual/big-query-table_test.go +++ b/sources/gcp/manual/big-query-table_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -51,30 +51,18 @@ func TestBigQueryTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryConnection.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us", "test-connection"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -104,30 +92,18 @@ func TestBigQueryTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryConnection.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us", "test-connection"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -224,6 +200,49 @@ func TestBigQueryTable(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockBigQueryTableClient(ctrl) + projectID := "cache-test-project" + scope := projectID + datasetID := "empty_dataset" + query := datasetID + + mockClient.EXPECT().List(gomock.Any(), projectID, datasetID, gomock.Any()).Return([]*sdp.Item{}, nil).Times(1) + + wrapper := manual.NewBigQueryTable(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("List_Unsupported", func(t *testing.T) { wrapper := manual.NewBigQueryTable(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/certificate-manager-certificate.go b/sources/gcp/manual/certificate-manager-certificate.go new file mode 100644 index 00000000..43bb47b2 --- /dev/null +++ b/sources/gcp/manual/certificate-manager-certificate.go @@ -0,0 +1,296 @@ +package manual + +import ( + "context" + "errors" + + certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" + "google.golang.org/api/iterator" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ( + CertificateManagerCertificateLookupByLocation = shared.NewItemTypeLookup("location", gcpshared.CertificateManagerCertificate) + CertificateManagerCertificateLookupByName = shared.NewItemTypeLookup("name", gcpshared.CertificateManagerCertificate) +) + +type certificateManagerCertificateWrapper struct { + client gcpshared.CertificateManagerCertificateClient + *gcpshared.ProjectBase +} + +// NewCertificateManagerCertificate creates a new certificateManagerCertificateWrapper. +func NewCertificateManagerCertificate(client gcpshared.CertificateManagerCertificateClient, locations []gcpshared.LocationInfo) sources.SearchableWrapper { + return &certificateManagerCertificateWrapper{ + client: client, + ProjectBase: gcpshared.NewProjectBase( + locations, + sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, + gcpshared.CertificateManagerCertificate, + ), + } +} + +func (c certificateManagerCertificateWrapper) IAMPermissions() []string { + return []string{ + "certificatemanager.certs.get", + "certificatemanager.certs.list", + } +} + +func (c certificateManagerCertificateWrapper) PredefinedRole() string { + return "roles/certificatemanager.viewer" +} + +func (c certificateManagerCertificateWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + gcpshared.CertificateManagerDnsAuthorization, + gcpshared.CertificateManagerCertificateIssuanceConfig, + stdlib.NetworkDNS, + ) +} + +func (c certificateManagerCertificateWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + // https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/certificate_manager_certificate + // ID format: projects/{{project}}/locations/{{location}}/certificates/{{name}} + // The framework automatically intercepts queries starting with "projects/" and converts + // them to GET operations by extracting the last N path parameters (based on GetLookups count). + TerraformQueryMap: "google_certificate_manager_certificate.id", + }, + } +} + +func (c certificateManagerCertificateWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + CertificateManagerCertificateLookupByLocation, + CertificateManagerCertificateLookupByName, + } +} + +// Get retrieves a Certificate Manager Certificate by its unique attribute (location|certificateName). +func (c certificateManagerCertificateWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + location, err := c.LocationFromScope(scope) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + } + } + + if len(queryParts) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires exactly 2 query parts: location and certificate name", + } + } + + locationName := queryParts[0] + certificateName := queryParts[1] + + // Construct the full resource name + // Format: projects/{project}/locations/{location}/certificates/{certificate} + name := "projects/" + location.ProjectID + "/locations/" + locationName + "/certificates/" + certificateName + + req := &certificatemanagerpb.GetCertificateRequest{ + Name: name, + } + + certificate, getErr := c.client.GetCertificate(ctx, req) + if getErr != nil { + return nil, gcpshared.QueryError(getErr, scope, c.Type()) + } + + item, sdpErr := c.gcpCertificateToSDPItem(certificate, location) + if sdpErr != nil { + return nil, sdpErr + } + + return item, nil +} + +func (c certificateManagerCertificateWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + CertificateManagerCertificateLookupByLocation, + }, + } +} + +// Search searches Certificate Manager Certificates by location. +func (c certificateManagerCertificateWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + return gcpshared.CollectFromStream(ctx, func(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey) { + c.SearchStream(ctx, stream, cache, cacheKey, scope, queryParts...) + }) +} + +// SearchStream streams certificates matching the search criteria (location). +func (c certificateManagerCertificateWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + location, err := c.LocationFromScope(scope) + if err != nil { + stream.SendError(&sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + }) + return + } + + if len(queryParts) != 1 { + stream.SendError(&sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: location", + }) + return + } + + locationName := queryParts[0] + + // Construct the parent path + // Format: projects/{project}/locations/{location} + parent := "projects/" + location.ProjectID + "/locations/" + locationName + + req := &certificatemanagerpb.ListCertificatesRequest{ + Parent: parent, + } + + results := c.client.ListCertificates(ctx, req) + + for { + cert, iterErr := results.Next() + if errors.Is(iterErr, iterator.Done) { + break + } + if iterErr != nil { + stream.SendError(gcpshared.QueryError(iterErr, scope, c.Type())) + return + } + + item, sdpErr := c.gcpCertificateToSDPItem(cert, location) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } +} + +func (c certificateManagerCertificateWrapper) gcpCertificateToSDPItem(certificate *certificatemanagerpb.Certificate, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(certificate, "labels") + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + } + } + + // Extract location and certificate name from the resource name + // Format: projects/{project}/locations/{location}/certificates/{certificate} + values := gcpshared.ExtractPathParams(certificate.GetName(), "locations", "certificates") + if len(values) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "invalid certificate name format: " + certificate.GetName(), + } + } + + locationName := values[0] + certificateName := values[1] + + // Set composite unique attribute + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(locationName, certificateName)) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + } + } + + sdpItem := &sdp.Item{ + Type: gcpshared.CertificateManagerCertificate.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: location.ToScope(), + Tags: certificate.GetLabels(), + } + + // Link to DNS names from sanDnsNames (covers both managed and self-managed certificates) + for _, dnsName := range certificate.GetSanDnsnames() { + if dnsName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: dnsName, + Scope: "global", + }, + }) + } + } + + // Link to DNS Authorizations used for managed certificate domain validation + if managed := certificate.GetManaged(); managed != nil { + // Link to DNS names from managed.domains + for _, domain := range managed.GetDomains() { + if domain != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: domain, + Scope: "global", + }, + }) + } + } + for _, dnsAuthURI := range managed.GetDnsAuthorizations() { + // Extract location and dnsAuthorization name from URI + // Format: projects/{project}/locations/{location}/dnsAuthorizations/{dnsAuthorization} + values := gcpshared.ExtractPathParams(dnsAuthURI, "locations", "dnsAuthorizations") + if len(values) == 2 && values[0] != "" && values[1] != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.CertificateManagerDnsAuthorization.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(values[0], values[1]), + Scope: location.ProjectID, + }, + }) + } + } + + // Link to Certificate Issuance Config for private PKI certificates + if issuanceConfigURI := managed.GetIssuanceConfig(); issuanceConfigURI != "" { + // Extract location and issuanceConfig name from URI + // Format: projects/{project}/locations/{location}/certificateIssuanceConfigs/{certificateIssuanceConfig} + values := gcpshared.ExtractPathParams(issuanceConfigURI, "locations", "certificateIssuanceConfigs") + if len(values) == 2 && values[0] != "" && values[1] != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.CertificateManagerCertificateIssuanceConfig.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(values[0], values[1]), + Scope: location.ProjectID, + }, + }) + } + } + } + + // Note: The Certificate resource's UsedBy field (which lists resources using this certificate) + // is not available in the Go SDK protobuf. The reverse links from CertificateMap, + // CertificateMapEntry, and TargetHttpsProxy to Certificate will be established + // when those adapters are created. + + return sdpItem, nil +} diff --git a/sources/gcp/manual/certificate-manager-certificate_test.go b/sources/gcp/manual/certificate-manager-certificate_test.go new file mode 100644 index 00000000..dd2ff0fd --- /dev/null +++ b/sources/gcp/manual/certificate-manager-certificate_test.go @@ -0,0 +1,265 @@ +package manual_test + +import ( + "context" + "testing" + + certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" + "go.uber.org/mock/gomock" + "google.golang.org/api/iterator" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/gcp/manual" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/gcp/shared/mocks" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +func createCertificate(projectID, location, name string) *certificatemanagerpb.Certificate { + return &certificatemanagerpb.Certificate{ + Name: "projects/" + projectID + "/locations/" + location + "/certificates/" + name, + Description: "Test certificate", + CreateTime: timestamppb.Now(), + UpdateTime: timestamppb.Now(), + Labels: map[string]string{ + "env": "test", + }, + Scope: certificatemanagerpb.Certificate_DEFAULT, + } +} + +func TestCertificateManagerCertificate(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mocks.NewMockCertificateManagerCertificateClient(ctrl) + projectID := "test-project-id" + location := "us-central1" + certificateName := "test-certificate" + + t.Run("Get", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + mockClient.EXPECT().GetCertificate(ctx, gomock.Any()).Return(createCertificate(projectID, location, certificateName), nil) + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], location+shared.QuerySeparator+certificateName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatal("Expected item, got nil") + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + // Verify the item type + if sdpItem.GetType() != gcpshared.CertificateManagerCertificate.String() { + t.Errorf("Expected type %s, got: %s", gcpshared.CertificateManagerCertificate.String(), sdpItem.GetType()) + } + + // Verify the unique attribute + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got: %s", sdpItem.GetUniqueAttribute()) + } + + // Verify the scope + expectedScope := projectID + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got: %s", expectedScope, sdpItem.GetScope()) + } + }) + + t.Run("Search", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + mockIterator := mocks.NewMockCertificateIterator(ctrl) + + mockIterator.EXPECT().Next().Return(createCertificate(projectID, location, "cert1"), nil) + mockIterator.EXPECT().Next().Return(createCertificate(projectID, location, "cert2"), nil) + mockIterator.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().ListCertificates(ctx, gomock.Any()).Return(mockIterator) + + // Check if adapter supports searching + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], location, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + expectedCount := 2 + actualCount := len(sdpItems) + if actualCount != expectedCount { + t.Fatalf("Expected %d items, got: %d", expectedCount, actualCount) + } + + for _, item := range sdpItems { + if err := item.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + } + }) + + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockCertificateManagerCertificateClient(ctrl) + projectID := "cache-test-project" + scope := projectID + locationName := "us-central1" + query := locationName + + mockIter := mocks.NewMockCertificateIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().ListCertificates(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + + t.Run("GetLookups", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + lookups := wrapper.GetLookups() + if len(lookups) != 2 { + t.Errorf("Expected 2 lookups, got: %d", len(lookups)) + } + + // Verify the lookup types + expectedTypes := []string{"location", "name"} + for i, lookup := range lookups { + if lookup.By != expectedTypes[i] { + t.Errorf("Expected lookup by %s, got: %s", expectedTypes[i], lookup.By) + } + } + }) + + t.Run("SearchLookups", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + searchLookups := wrapper.SearchLookups() + if len(searchLookups) != 1 { + t.Errorf("Expected 1 search lookup, got: %d", len(searchLookups)) + } + + // Verify the search lookup has only location + if len(searchLookups[0]) != 1 { + t.Errorf("Expected 1 lookup in search lookup, got: %d", len(searchLookups[0])) + } + }) + + t.Run("TerraformMappings", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + mappings := wrapper.TerraformMappings() + if len(mappings) != 1 { + t.Errorf("Expected 1 terraform mapping, got: %d", len(mappings)) + } + + mapping := mappings[0] + if mapping.GetTerraformMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Expected SEARCH method, got: %v", mapping.GetTerraformMethod()) + } + + expectedQueryMap := "google_certificate_manager_certificate.id" + if mapping.GetTerraformQueryMap() != expectedQueryMap { + t.Errorf("Expected query map %s, got: %s", expectedQueryMap, mapping.GetTerraformQueryMap()) + } + }) + + t.Run("IAMPermissions", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + permissions := wrapper.IAMPermissions() + expectedPermissions := []string{ + "certificatemanager.certs.get", + "certificatemanager.certs.list", + } + + if len(permissions) != len(expectedPermissions) { + t.Errorf("Expected %d permissions, got: %d", len(expectedPermissions), len(permissions)) + } + + for i, perm := range permissions { + if perm != expectedPermissions[i] { + t.Errorf("Expected permission %s, got: %s", expectedPermissions[i], perm) + } + } + }) + + t.Run("PredefinedRole", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + // PredefinedRole is available on the wrapper, not the adapter + role := wrapper.(interface{ PredefinedRole() string }).PredefinedRole() + expectedRole := "roles/certificatemanager.viewer" + if role != expectedRole { + t.Errorf("Expected role %s, got: %s", expectedRole, role) + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + links := wrapper.PotentialLinks() + expectedLinks := map[shared.ItemType]bool{ + gcpshared.CertificateManagerDnsAuthorization: true, + gcpshared.CertificateManagerCertificateIssuanceConfig: true, + stdlib.NetworkDNS: true, + } + + if len(links) != len(expectedLinks) { + t.Errorf("Expected %d potential links, got: %d", len(expectedLinks), len(links)) + } + + for expectedLink := range expectedLinks { + if !links[expectedLink] { + t.Errorf("Expected link to %s", expectedLink) + } + } + }) +} diff --git a/sources/gcp/manual/cloud-kms-crypto-key-version.go b/sources/gcp/manual/cloud-kms-crypto-key-version.go index 302d2fdc..b6e7d755 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go index 9a48356f..e8c8c976 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -190,6 +190,51 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + defer cache.Clear() + + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no crypto key versions found for search query", + } + query := "us|test-keyring|empty-key" + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), query) + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, searchCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + wrapper := manual.NewCloudKMSCryptoKeyVersion(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + scope := wrapper.Scopes()[0] + + items, qErr := searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("first Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", cachedErr) + } + + items, qErr = searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("second Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("List_Unsupported", func(t *testing.T) { cache := sdpcache.NewMemoryCache() defer cache.Clear() @@ -351,10 +396,6 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { Query: "us|test-keyring|test-key", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -378,10 +419,6 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us|test-keyring|test-key", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/cloud-kms-crypto-key.go b/sources/gcp/manual/cloud-kms-crypto-key.go index 39a67a0f..c69cbb78 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key.go +++ b/sources/gcp/manual/cloud-kms-crypto-key.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key_test.go b/sources/gcp/manual/cloud-kms-crypto-key_test.go index 9d60c092..4bd7195f 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -187,6 +187,51 @@ func TestCloudKMSCryptoKey(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + defer cache.Clear() + + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no crypto keys found for search query", + } + query := "global|empty-keyring" + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKey.String(), query) + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, searchCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + wrapper := manual.NewCloudKMSCryptoKey(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + scope := wrapper.Scopes()[0] + + items, qErr := searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("first Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", cachedErr) + } + + items, qErr = searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("second Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search_TerraformFormat", func(t *testing.T) { cache := sdpcache.NewCache(ctx) defer cache.Clear() @@ -349,10 +394,6 @@ func TestCloudKMSCryptoKey(t *testing.T) { Query: "global|test-keyring|test-key", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -361,10 +402,6 @@ func TestCloudKMSCryptoKey(t *testing.T) { Query: "global|test-keyring", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -373,10 +410,6 @@ func TestCloudKMSCryptoKey(t *testing.T) { Query: "global|test-keyring|test-key", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } @@ -400,30 +433,18 @@ func TestCloudKMSCryptoKey(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSKeyRing.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "global|test-keyring|test-key", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/manual/cloud-kms-key-ring.go b/sources/gcp/manual/cloud-kms-key-ring.go index fcba983a..ef91a848 100644 --- a/sources/gcp/manual/cloud-kms-key-ring.go +++ b/sources/gcp/manual/cloud-kms-key-ring.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-key-ring_test.go b/sources/gcp/manual/cloud-kms-key-ring_test.go index db4b413c..713d169b 100644 --- a/sources/gcp/manual/cloud-kms-key-ring_test.go +++ b/sources/gcp/manual/cloud-kms-key-ring_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -181,6 +181,50 @@ func TestCloudKMSKeyRing(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + defer cache.Clear() + + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no key rings found for list", + } + listCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_LIST, projectID, gcpshared.CloudKMSKeyRing.String(), "") + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, listCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + wrapper := manual.NewCloudKMSKeyRing(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + scope := wrapper.Scopes()[0] + + items, qErr := listable.List(ctx, scope, false) + if qErr != nil { + t.Fatalf("first List: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first List: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List, got %v", cachedErr) + } + + items, qErr = listable.List(ctx, scope, false) + if qErr != nil { + t.Fatalf("second List: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second List: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search_CacheHit_ByLocation", func(t *testing.T) { cache := sdpcache.NewCache(ctx) defer cache.Clear() @@ -222,6 +266,51 @@ func TestCloudKMSKeyRing(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + defer cache.Clear() + + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no key rings found for search query", + } + query := "us-central1" + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSKeyRing.String(), query) + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, searchCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + wrapper := manual.NewCloudKMSKeyRing(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + scope := wrapper.Scopes()[0] + + items, qErr := searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("first Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", cachedErr) + } + + items, qErr = searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("second Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search_TerraformFormat", func(t *testing.T) { cache := sdpcache.NewCache(ctx) defer cache.Clear() @@ -353,10 +442,6 @@ func TestCloudKMSKeyRing(t *testing.T) { Query: "us|test-keyring", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -365,10 +450,6 @@ func TestCloudKMSKeyRing(t *testing.T) { Query: "us|test-keyring", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, }, } @@ -392,20 +473,12 @@ func TestCloudKMSKeyRing(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us|test-keyring", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "us|test-keyring", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } diff --git a/sources/gcp/manual/compute-address.go b/sources/gcp/manual/compute-address.go index 81844d23..3e5f8735 100644 --- a/sources/gcp/manual/compute-address.go +++ b/sources/gcp/manual/compute-address.go @@ -5,15 +5,16 @@ import ( "errors" "fmt" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -137,6 +138,8 @@ func (c computeAddressWrapper) ListStream(ctx context.Context, stream discovery. Region: location.Region, }) + var itemsSent int + var hadError bool for { address, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -150,11 +153,24 @@ func (c computeAddressWrapper) ListStream(ctx context.Context, stream discovery. item, sdpErr := c.gcpComputeAddressToSDPItem(ctx, address, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute addresses found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -165,6 +181,8 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -180,6 +198,7 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -200,11 +219,13 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream item, sdpErr := c.gcpComputeAddressToSDPItem(ctx, address, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -215,6 +236,17 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute addresses found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, address *computepb.Address, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -246,10 +278,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a Query: networkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -267,10 +295,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a Query: subnetworkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -284,10 +308,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a Query: ip, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -298,7 +318,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a ctx, location.ProjectID, userURI, - &sdp.BlastPropagation{In: true, Out: true}, ) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) @@ -319,10 +338,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a Query: prefixName, Scope: fmt.Sprintf("%s.%s", location.ProjectID, region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-address_test.go b/sources/gcp/manual/compute-address_test.go index 955f81d3..8171cd94 100644 --- a/sources/gcp/manual/compute-address_test.go +++ b/sources/gcp/manual/compute-address_test.go @@ -6,14 +6,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -54,30 +55,18 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -179,6 +168,77 @@ func TestComputeAddress(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeAddressClient(ctrl) + projectID := "cache-test-project" + region := "us-central1" + scope := projectID + "." + region + + mockAggIter := mocks.NewMockAddressesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.AddressesScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeAddressIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeAddress(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + t.Run("GetWithUsers", func(t *testing.T) { wrapper := manual.NewComputeAddress(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) @@ -213,10 +273,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -224,10 +280,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IP address link { @@ -235,10 +287,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Regional forwarding rule link (from users) { @@ -246,10 +294,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-forwarding-rule", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Global forwarding rule link (from users) { @@ -257,10 +301,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-global-forwarding-rule", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Instance link (from users) { @@ -268,10 +308,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.us-central1-a", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Target VPN Gateway link (from users) { @@ -279,10 +315,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vpn-gateway", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Router link (from users) { @@ -290,10 +322,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-router", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -323,10 +351,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -334,10 +358,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IP address link { @@ -345,10 +365,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Public Delegated Prefix link (from ipCollection) { @@ -356,10 +372,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-prefix", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-autoscaler.go b/sources/gcp/manual/compute-autoscaler.go index 8885aeff..18923b2a 100644 --- a/sources/gcp/manual/compute-autoscaler.go +++ b/sources/gcp/manual/compute-autoscaler.go @@ -4,15 +4,16 @@ import ( "context" "errors" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -130,6 +131,8 @@ func (c computeAutoscalerWrapper) ListStream(ctx context.Context, stream discove Zone: location.Zone, }) + var itemsSent int + var hadError bool for { autoscaler, iterErr := results.Next() if errors.Is(iterErr, iterator.Done) { @@ -143,11 +146,24 @@ func (c computeAutoscalerWrapper) ListStream(ctx context.Context, stream discove item, sdpErr := c.gcpComputeAutoscalerToSDPItem(ctx, autoscaler, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute autoscalers found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -158,6 +174,8 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -173,6 +191,7 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -193,11 +212,13 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre item, sdpErr := c.gcpComputeAutoscalerToSDPItem(ctx, autoscaler, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -208,6 +229,17 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute autoscalers found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeAutoscalerWrapper) gcpComputeAutoscalerToSDPItem(ctx context.Context, autoscaler *computepb.Autoscaler, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -240,10 +272,6 @@ func (c computeAutoscalerWrapper) gcpComputeAutoscalerToSDPItem(ctx context.Cont Query: igmName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/compute-autoscaler_test.go b/sources/gcp/manual/compute-autoscaler_test.go index 9fd6814a..67ab1aa3 100644 --- a/sources/gcp/manual/compute-autoscaler_test.go +++ b/sources/gcp/manual/compute-autoscaler_test.go @@ -5,14 +5,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -89,10 +90,6 @@ func TestComputeAutoscalerWrapper(t *testing.T) { // [SPEC] Autoscalers are tightly coupled with the instance group manager // (albeit less strength on the IN direction). - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -183,6 +180,77 @@ func TestComputeAutoscalerWrapper(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeAutoscalerClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockAutoscalersScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.AutoscalersScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeAutoscalerIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeAutoscaler(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } // Create an autoscaler fixture (as returned from GCP API). diff --git a/sources/gcp/manual/compute-backend-service.go b/sources/gcp/manual/compute-backend-service.go index f70cd8a2..d61a47dd 100644 --- a/sources/gcp/manual/compute-backend-service.go +++ b/sources/gcp/manual/compute-backend-service.go @@ -5,15 +5,16 @@ import ( "errors" "fmt" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -189,6 +190,8 @@ func (c computeBackendServiceWrapper) ListStream(ctx context.Context, stream dis } // Route to the appropriate API based on whether the scope includes a region + var itemsSent int + var hadError bool if location.Regional() { // Regional backend services it := c.regionalClient.List(ctx, &computepb.ListRegionBackendServicesRequest{ @@ -209,11 +212,13 @@ func (c computeBackendServiceWrapper) ListStream(ctx context.Context, stream dis item, sdpErr := gcpComputeBackendServiceToSDPItem(ctx, location.ProjectID, location.ToScope(), backendService, gcpshared.ComputeBackendService) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ } } else { // Global backend services @@ -234,13 +239,26 @@ func (c computeBackendServiceWrapper) ListStream(ctx context.Context, stream dis item, sdpErr := gcpComputeBackendServiceToSDPItem(ctx, location.ProjectID, location.ToScope(), backendService, gcpshared.ComputeBackendService) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ } } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute backend services found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } // listAggregatedStream uses AggregatedList to stream all backend services across all regions (global and regional) @@ -250,6 +268,8 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -265,6 +285,7 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -285,11 +306,13 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, item, sdpErr := gcpComputeBackendServiceToSDPItem(ctx, scopeLocation.ProjectID, scopeLocation.ToScope(), backendService, gcpshared.ComputeBackendService) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -300,6 +323,17 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute backend services found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, scope string, bs *computepb.BackendService, itemType shared.ItemType) (*sdp.Item, *sdp.QueryError) { @@ -333,10 +367,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc // This is a global resource Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -359,10 +389,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: securityPolicyName, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -378,10 +404,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: edgeSecurityPolicyName, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -410,10 +432,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: shared.CompositeLookupKey(location, policyName), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -448,10 +466,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: healthCheckName, Scope: healthCheckScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -475,10 +489,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: groupName, Scope: zone, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -502,10 +512,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: negName, Scope: zone, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } else { @@ -519,10 +525,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: negName, Scope: negScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -541,10 +543,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: groupName, Scope: zone, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -567,10 +565,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: shared.CompositeLookupKey(location, policyName), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -593,10 +587,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: shared.CompositeLookupKey(location, bindingName), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -624,10 +614,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: negName, Scope: negScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -653,10 +639,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: instanceName, Scope: instanceScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -681,10 +663,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc // Regions are project-scoped resources Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-backend-service_test.go b/sources/gcp/manual/compute-backend-service_test.go index 9b32a63f..0a781f0f 100644 --- a/sources/gcp/manual/compute-backend-service_test.go +++ b/sources/gcp/manual/compute-backend-service_test.go @@ -7,14 +7,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -122,60 +123,36 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -276,6 +253,48 @@ func TestComputeBackendService(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockGlobalClient := mocks.NewMockComputeBackendServiceClient(ctrl) + mockRegionalClient := mocks.NewMockComputeRegionBackendServiceClient(ctrl) + projectID := "cache-test-project" + + mockAggIter := mocks.NewMockBackendServicesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.BackendServicesScopedListPair{}, iterator.Done) + mockGlobalClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + + wrapper := manual.NewComputeBackendService(mockGlobalClient, mockRegionalClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}, nil) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + }) + t.Run("GetWithHealthCheck", func(t *testing.T) { wrapper := manual.NewComputeBackendService(mockGlobalClient, mockRegionalClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}, nil) @@ -300,70 +319,42 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeHealthCheck.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-health-check", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -396,70 +387,42 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeHealthCheck.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-regional-health-check", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -496,70 +459,42 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance-group", ExpectedScope: zone, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -600,80 +535,48 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetworkEndpointGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-neg", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -706,70 +609,42 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: region, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-disk.go b/sources/gcp/manual/compute-disk.go index 04350018..697e074c 100644 --- a/sources/gcp/manual/compute-disk.go +++ b/sources/gcp/manual/compute-disk.go @@ -4,15 +4,16 @@ import ( "context" "errors" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -135,6 +136,8 @@ func (c computeDiskWrapper) ListStream(ctx context.Context, stream discovery.Que Zone: location.Zone, }) + var itemsSent int + var hadError bool for { disk, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -148,11 +151,24 @@ func (c computeDiskWrapper) ListStream(ctx context.Context, stream discovery.Que item, sdpErr := c.gcpComputeDiskToSDPItem(ctx, disk, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute disks found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -163,6 +179,8 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -178,6 +196,7 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -198,11 +217,13 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis item, sdpErr := c.gcpComputeDiskToSDPItem(ctx, disk, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -213,6 +234,17 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute disks found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *computepb.Disk, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -248,10 +280,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: diskTypeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -273,10 +301,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: sourceImage, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -298,10 +322,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: snapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -324,10 +344,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: instantSnapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -350,10 +366,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: sourceDiskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -376,10 +388,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: rpName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -401,10 +409,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: instanceName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } @@ -433,10 +437,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c }, // Deleting a key might break the disk’s ability to function and have its data read // Deleting a disk in GCP does not affect its associated encryption key - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -464,10 +464,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c }, // Deleting a key might break the disk’s ability to function and have its data read // Deleting a disk in GCP does not affect its source image's encryption key - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -495,10 +491,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c }, // Deleting a key might break the disk’s ability to function and have its data read // Deleting a disk in GCP does not affect its source image's encryption key - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -520,10 +512,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: rpName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -543,9 +531,8 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c // - gs://bucket-name/path/to/file // - bucket-name (without gs:// prefix) if sourceStorageObject := disk.GetSourceStorageObject(); sourceStorageObject != "" { - blastPropagation := &sdp.BlastPropagation{In: true, Out: false} if linkFunc, ok := gcpshared.ManualAdapterLinksByAssetType[gcpshared.StorageBucket]; ok { - linkedQuery := linkFunc(location.ProjectID, location.ToScope(), sourceStorageObject, blastPropagation) + linkedQuery := linkFunc(location.ProjectID, location.ToScope(), sourceStorageObject) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) } @@ -569,10 +556,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Scope: scope, }, // If the Storage Pool is deleted or updated: The disk may fail to operate correctly or become invalid. If the disk is updated: The Storage Pool remains unaffected. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -594,10 +577,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: primaryDiskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -617,10 +596,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: policyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -644,10 +619,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: secondaryDiskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -667,10 +638,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: policyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-disk_test.go b/sources/gcp/manual/compute-disk_test.go index f406f199..4fcd801d 100644 --- a/sources/gcp/manual/compute-disk_test.go +++ b/sources/gcp/manual/compute-disk_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -67,7 +67,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, }, }, @@ -81,7 +80,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, }, }, @@ -95,7 +93,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instant-snapshot", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, }, }, @@ -109,7 +106,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, }, }, @@ -121,49 +117,42 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } userTest := shared.QueryTest{ ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, } diskTypeTest := shared.QueryTest{ ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } diskEncryptionKeyTest := shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } sourceImageEncryptionKeyTest := shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } sourceSnapshotEncryptionKeyTest := shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } sourceConsistencyGroupPolicy := shared.QueryTest{ ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } for _, tc := range cases { @@ -339,6 +328,77 @@ func TestComputeDisk(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeDiskClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockDisksScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.DisksScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeDiskIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeDisk(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + t.Run("GetWithSourceStorageObject", func(t *testing.T) { wrapper := manual.NewComputeDisk(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) @@ -364,56 +424,48 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, { ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -423,7 +475,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -454,56 +505,48 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, { ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -513,7 +556,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-storage-pool", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -548,56 +590,48 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, { ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -608,14 +642,12 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "primary-disk", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, ) @@ -661,56 +693,48 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, { ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -721,21 +745,18 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "secondary-disk-1", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "secondary-disk-2", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, ) diff --git a/sources/gcp/manual/compute-forwarding-rule.go b/sources/gcp/manual/compute-forwarding-rule.go index a8a967a4..2eecc0ac 100644 --- a/sources/gcp/manual/compute-forwarding-rule.go +++ b/sources/gcp/manual/compute-forwarding-rule.go @@ -4,15 +4,16 @@ import ( "context" "errors" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -142,6 +143,8 @@ func (c computeForwardingRuleWrapper) ListStream(ctx context.Context, stream dis Region: location.Region, }) + var itemsSent int + var hadError bool for { rule, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -155,11 +158,24 @@ func (c computeForwardingRuleWrapper) ListStream(ctx context.Context, stream dis item, sdpErr := c.gcpComputeForwardingRuleToSDPItem(ctx, rule, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute forwarding rules found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -170,6 +186,8 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -185,6 +203,7 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -205,11 +224,13 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, item, sdpErr := c.gcpComputeForwardingRuleToSDPItem(ctx, rule, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -220,6 +241,17 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute forwarding rules found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx context.Context, rule *computepb.ForwardingRule, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -247,10 +279,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: rule.GetIPAddress(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -266,10 +294,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: backendServiceName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -303,10 +327,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: networkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -324,10 +344,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: subnetworkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -335,10 +351,7 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont // Link to target resource (polymorphic) if target := rule.GetTarget(); target != "" { - linkedQuery := gcpshared.ForwardingRuleTargetLinker(location.ProjectID, location.ToScope(), target, &sdp.BlastPropagation{ - In: true, - Out: true, - }) + linkedQuery := gcpshared.ForwardingRuleTargetLinker(location.ProjectID, location.ToScope(), target) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) } @@ -357,10 +370,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: forwardingRuleName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -379,10 +388,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: prefixName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -402,10 +407,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: query, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -424,10 +425,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: query, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-forwarding-rule_test.go b/sources/gcp/manual/compute-forwarding-rule_test.go index 56d86fdd..976cbea9 100644 --- a/sources/gcp/manual/compute-forwarding-rule_test.go +++ b/sources/gcp/manual/compute-forwarding-rule_test.go @@ -6,14 +6,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -54,40 +55,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -187,6 +172,77 @@ func TestComputeForwardingRule(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeForwardingRuleClient(ctrl) + projectID := "cache-test-project" + region := "us-central1" + scope := projectID + "." + region + + mockAggIter := mocks.NewMockForwardingRulesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.ForwardingRulesScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockForwardingRuleIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeForwardingRule(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + t.Run("GetWithTarget", func(t *testing.T) { wrapper := manual.NewComputeForwardingRule(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) @@ -212,40 +268,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -255,10 +295,6 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-target-proxy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -289,40 +325,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -332,10 +352,6 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "base-forwarding-rule", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -366,40 +382,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -409,10 +409,6 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-prefix", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -449,40 +445,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -493,20 +473,12 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1|test-namespace", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.ServiceDirectoryService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1|test-namespace|test-service", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, ) diff --git a/sources/gcp/manual/compute-healthcheck.go b/sources/gcp/manual/compute-healthcheck.go index 27bbb796..b24e0ae6 100644 --- a/sources/gcp/manual/compute-healthcheck.go +++ b/sources/gcp/manual/compute-healthcheck.go @@ -5,15 +5,16 @@ import ( "errors" "fmt" "net" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -183,6 +184,8 @@ func (c computeHealthCheckWrapper) ListStream(ctx context.Context, stream discov } // Route to the appropriate API based on whether the scope includes a region + var itemsSent int + var hadError bool if location.Regional() { // Regional health checks it := c.regionalClient.List(ctx, &computepb.ListRegionHealthChecksRequest{ @@ -203,11 +206,13 @@ func (c computeHealthCheckWrapper) ListStream(ctx context.Context, stream discov item, sdpErr := GcpComputeHealthCheckToSDPItem(healthCheck, location, gcpshared.ComputeHealthCheck) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ } } else { // Global health checks @@ -228,13 +233,26 @@ func (c computeHealthCheckWrapper) ListStream(ctx context.Context, stream discov item, sdpErr := GcpComputeHealthCheckToSDPItem(healthCheck, location, gcpshared.ComputeHealthCheck) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ } } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute health checks found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } // listAggregatedStream uses AggregatedList to stream all health checks across all regions (global and regional) @@ -244,6 +262,8 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -259,6 +279,7 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -279,11 +300,13 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str item, sdpErr := GcpComputeHealthCheckToSDPItem(healthCheck, scopeLocation, gcpshared.ComputeHealthCheck) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -294,6 +317,17 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute health checks found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } // GcpComputeHealthCheckToSDPItem converts a GCP health check to an SDP item. @@ -345,10 +379,6 @@ func GcpComputeHealthCheckToSDPItem(healthCheck *computepb.HealthCheck, location Query: regionName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -362,10 +392,6 @@ func GcpComputeHealthCheckToSDPItem(healthCheck *computepb.HealthCheck, location Query: region, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -385,10 +411,6 @@ func linkHostToNetworkResource(sdpItem *sdp.Item, host string) { Query: host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } else { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -398,10 +420,6 @@ func linkHostToNetworkResource(sdpItem *sdp.Item, host string) { Query: host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/compute-healthcheck_test.go b/sources/gcp/manual/compute-healthcheck_test.go index 61ccff56..5df60f4f 100644 --- a/sources/gcp/manual/compute-healthcheck_test.go +++ b/sources/gcp/manual/compute-healthcheck_test.go @@ -7,14 +7,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -148,10 +149,6 @@ func TestComputeHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -180,10 +177,6 @@ func TestComputeHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.100", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -212,30 +205,18 @@ func TestComputeHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-east1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "europe-west1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -264,10 +245,6 @@ func TestComputeHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -359,6 +336,49 @@ func TestComputeHealthCheck(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockGlobalClient := mocks.NewMockComputeHealthCheckClient(ctrl) + mockRegionalClient := mocks.NewMockComputeRegionHealthCheckClient(ctrl) + projectID := "cache-test-project" + + mockAggIter := mocks.NewMockHealthChecksScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.HealthChecksScopedListPair{}, iterator.Done) + mockGlobalClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + + // Project-only: List("*") uses AggregatedList; we only test the "*" path here. + wrapper := manual.NewComputeHealthCheck(mockGlobalClient, mockRegionalClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}, nil) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + }) + // Regional health check tests region := "us-central1" diff --git a/sources/gcp/manual/compute-image.go b/sources/gcp/manual/compute-image.go index 4f6c68e8..c2fc5784 100644 --- a/sources/gcp/manual/compute-image.go +++ b/sources/gcp/manual/compute-image.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -201,6 +201,8 @@ func (c computeImageWrapper) ListStream(ctx context.Context, stream discovery.Qu Project: location.ProjectID, }) + var itemsSent int + var hadError bool for { image, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -214,11 +216,24 @@ func (c computeImageWrapper) ListStream(ctx context.Context, stream discovery.Qu item, sdpErr := c.gcpComputeImageToSDPItem(ctx, image, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute images found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -263,10 +278,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -283,10 +294,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: snapshotName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -306,10 +313,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: sourceImage, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -329,10 +332,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: licenseName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -340,9 +339,8 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image // Link to raw disk storage bucket if rawDisk := image.GetRawDisk(); rawDisk != nil { if rawDiskSource := rawDisk.GetSource(); rawDiskSource != "" { - blastPropagation := &sdp.BlastPropagation{In: true, Out: false} if linkFunc, ok := gcpshared.ManualAdapterLinksByAssetType[gcpshared.StorageBucket]; ok { - linkedQuery := linkFunc(location.ProjectID, location.ToScope(), rawDiskSource, blastPropagation) + linkedQuery := linkFunc(location.ProjectID, location.ToScope(), rawDiskSource) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) } @@ -381,10 +379,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: replacement, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -407,10 +401,6 @@ func (c computeImageWrapper) addKMSKeyLinks(sdpItem *sdp.Item, keyName, kmsKeySe Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } else if loc != "" && keyRing != "" && cryptoKey != "" { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -420,10 +410,6 @@ func (c computeImageWrapper) addKMSKeyLinks(sdpItem *sdp.Item, keyName, kmsKeySe Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -448,10 +434,6 @@ func (c computeImageWrapper) addKMSKeyLinks(sdpItem *sdp.Item, keyName, kmsKeySe Query: serviceAccountEmail, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-image_test.go b/sources/gcp/manual/compute-image_test.go index 9b4e38f5..16567d49 100644 --- a/sources/gcp/manual/compute-image_test.go +++ b/sources/gcp/manual/compute-image_test.go @@ -2,6 +2,7 @@ package manual_test import ( "context" + "errors" "fmt" "sync" "testing" @@ -13,9 +14,9 @@ import ( "google.golang.org/grpc/status" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -55,10 +56,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-disk", ExpectedScope: fmt.Sprintf("%s.us-central1-a", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceSnapshot link { @@ -66,10 +63,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceImage link (SEARCH handles full URI; createComputeImageWithLinks uses https URL) { @@ -77,10 +70,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/test-source-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // licenses link (first license) { @@ -88,10 +77,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-license-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // licenses link (second license) { @@ -99,10 +84,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-license-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // rawDisk.source (GCS bucket) link { @@ -110,10 +91,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("%s-raw-disk-bucket", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // imageEncryptionKey.kmsKeyName (CryptoKeyVersion) link { @@ -121,10 +98,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-image-key|test-version-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // imageEncryptionKey.kmsKeyServiceAccount link { @@ -132,10 +105,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-image-kms-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceImageEncryptionKey.kmsKeyName (CryptoKeyVersion) link { @@ -143,10 +112,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-source-image-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceImageEncryptionKey.kmsKeyServiceAccount link { @@ -154,10 +119,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-source-image-kms-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceSnapshotEncryptionKey.kmsKeyName (CryptoKeyVersion) link { @@ -165,10 +126,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-source-snapshot-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceSnapshotEncryptionKey.kmsKeyServiceAccount link { @@ -176,10 +133,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-source-snapshot-kms-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // deprecated.replacement link (SEARCH handles full URI) { @@ -187,10 +140,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/test-replacement-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -340,6 +289,96 @@ func TestComputeImage(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeImagesClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockComputeImageIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeImage(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + + // SearchCachesNotFoundWithMemoryCache verifies that when Search returns no items + // (NotFound from both Get and GetFromFamily), NOTFOUND is cached. Second Search + // hits cache and returns 0 items without calling the backend again. + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeImagesClient(ctrl) + projectID := "cache-test-project" + scope := projectID + query := "nonexistent-image" + + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(nil, status.Error(codes.NotFound, "image not found")).Times(1) + mockClient.EXPECT().GetFromFamily(ctx, gomock.Any()).Return(nil, status.Error(codes.NotFound, "family not found")).Times(1) + + wrapper := manual.NewComputeImage(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + // First Search: cache miss → Get then GetFromFamily return NotFound → transformer stores NOTFOUND. + _, err := searchable.Search(ctx, scope, query, false) + if err == nil { + t.Fatal("first Search: expected error (NOTFOUND), got nil") + } + var qe *sdp.QueryError + if errors.As(err, &qe) && qe.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Search: expected NOTFOUND, got %v", err) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + // Second Search: cache hit → transformer returns empty result, no backend calls. + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search", func(t *testing.T) { t.Run("SearchByFamilyName", func(t *testing.T) { ctrl := gomock.NewController(t) diff --git a/sources/gcp/manual/compute-instance-group-manager-shared.go b/sources/gcp/manual/compute-instance-group-manager-shared.go index 839f70ef..adcf27c8 100644 --- a/sources/gcp/manual/compute-instance-group-manager-shared.go +++ b/sources/gcp/manual/compute-instance-group-manager-shared.go @@ -6,7 +6,7 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) @@ -51,7 +51,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: instanceTemplateName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -68,7 +67,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: instanceGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } } @@ -84,7 +82,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: zoneName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -100,7 +97,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: regionName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -118,7 +114,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: zoneName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -137,7 +132,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: targetPoolName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } } @@ -155,7 +149,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: resourcePolicyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -180,7 +173,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: versionTemplateName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -201,10 +193,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: healthCheckName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -224,7 +212,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: autoscalerName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } } diff --git a/sources/gcp/manual/compute-instance-group-manager.go b/sources/gcp/manual/compute-instance-group-manager.go index 15242e86..bb6cf3e5 100644 --- a/sources/gcp/manual/compute-instance-group-manager.go +++ b/sources/gcp/manual/compute-instance-group-manager.go @@ -3,15 +3,16 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -138,6 +139,8 @@ func (c computeInstanceGroupManagerWrapper) ListStream(ctx context.Context, stre Zone: location.Zone, }) + var itemsSent int + var hadError bool for { igm, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -151,11 +154,24 @@ func (c computeInstanceGroupManagerWrapper) ListStream(ctx context.Context, stre item, sdpErr := c.gcpInstanceGroupManagerToSDPItem(ctx, igm, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instance group managers found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -166,6 +182,8 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -181,6 +199,7 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -201,11 +220,13 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con item, sdpErr := c.gcpInstanceGroupManagerToSDPItem(ctx, igm, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -216,6 +237,17 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instance group managers found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeInstanceGroupManagerWrapper) gcpInstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *computepb.InstanceGroupManager, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-instance-group-manager_test.go b/sources/gcp/manual/compute-instance-group-manager_test.go index 913392ef..ea85e239 100644 --- a/sources/gcp/manual/compute-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-instance-group-manager_test.go @@ -6,14 +6,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -67,50 +68,30 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "unit-test-template", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeZone.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1-a", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -136,50 +117,30 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "unit-test-template", ExpectedScope: gcpshared.RegionalScope(projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeZone.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1-a", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -228,10 +189,6 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "canary-template", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Stable version template (regional) { @@ -239,40 +196,24 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "stable-template", ExpectedScope: gcpshared.RegionalScope(projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -318,10 +259,6 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "unit-test-template", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Health check from auto-healing policy { @@ -329,50 +266,30 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-health-check", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeZone.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1-a", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -508,6 +425,77 @@ func TestComputeInstanceGroupManager(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeInstanceGroupManagerClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockInstanceGroupManagersScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.InstanceGroupManagersScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeInstanceGroupManagerIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createInstanceGroupManager(name string, isStable bool, instanceTemplate string) *computepb.InstanceGroupManager { diff --git a/sources/gcp/manual/compute-instance-group.go b/sources/gcp/manual/compute-instance-group.go index 5665f459..82184f13 100644 --- a/sources/gcp/manual/compute-instance-group.go +++ b/sources/gcp/manual/compute-instance-group.go @@ -3,15 +3,16 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -128,6 +129,8 @@ func (c computeInstanceGroupWrapper) ListStream(ctx context.Context, stream disc Zone: location.Zone, }) + var itemsSent int + var hadError bool for { instanceGroup, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -141,11 +144,24 @@ func (c computeInstanceGroupWrapper) ListStream(ctx context.Context, stream disc item, sdpErr := c.gcpComputeInstanceGroupToSDPItem(instanceGroup, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instance groups found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -156,6 +172,8 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -171,6 +189,7 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -191,11 +210,13 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s item, sdpErr := c.gcpComputeInstanceGroupToSDPItem(instanceGroup, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -206,6 +227,17 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instance groups found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGroup *computepb.InstanceGroup, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -234,10 +266,6 @@ func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGr Query: networkName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -252,10 +280,6 @@ func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGr Query: subnetworkName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -270,10 +294,6 @@ func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGr Query: zoneName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -288,10 +308,6 @@ func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGr Query: regionName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-instance-group_test.go b/sources/gcp/manual/compute-instance-group_test.go index 6477324f..9fcfc6ef 100644 --- a/sources/gcp/manual/compute-instance-group_test.go +++ b/sources/gcp/manual/compute-instance-group_test.go @@ -6,14 +6,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -54,30 +55,18 @@ func TestComputeInstanceGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeZone.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: zone, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -169,6 +158,77 @@ func TestComputeInstanceGroup(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeInstanceGroupsClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockInstanceGroupsScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.InstanceGroupsScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeInstanceGroupIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeInstanceGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeInstanceGroup(name, network, subnetwork, projectID, zone string) *computepb.InstanceGroup { diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 46fadc07..240d3728 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -4,15 +4,16 @@ import ( "context" "errors" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -142,6 +143,8 @@ func (c computeInstanceWrapper) ListStream(ctx context.Context, stream discovery Zone: location.Zone, }) + itemsSent := 0 + var hadError bool for { instance, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -155,11 +158,25 @@ func (c computeInstanceWrapper) ListStream(ctx context.Context, stream discovery item, sdpErr := c.gcpComputeInstanceToSDPItem(ctx, instance, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instances found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -168,6 +185,8 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream // Get all unique project IDs projectIDs := gcpshared.GetProjectIDsFromLocations(c.Locations()) + var itemsSent atomic.Int32 + var hadError atomic.Bool // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) @@ -185,6 +204,7 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -205,11 +225,13 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream item, sdpErr := c.gcpComputeInstanceToSDPItem(ctx, instance, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -220,6 +242,18 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream // Wait for all goroutines to complete _ = p.Wait() + + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instances found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, instance *computepb.Instance, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -253,10 +287,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -277,10 +307,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: sourceImage, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -299,10 +325,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: snapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -324,10 +346,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -349,10 +367,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -376,10 +390,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } else { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -389,10 +399,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -410,10 +416,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: networkInterface.GetNetworkIP(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -425,10 +427,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: networkInterface.GetIpv6Address(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -442,10 +440,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: natIP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if externalIPv6 := accessConfig.GetExternalIpv6(); externalIPv6 != "" { @@ -456,10 +450,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: externalIPv6, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -474,10 +464,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: externalIPv6, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -494,10 +480,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: subnetworkName, Scope: gcpshared.RegionalScope(location.ProjectID, region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -514,10 +496,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: networkName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -539,10 +517,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: resourcePolicyName, Scope: gcpshared.RegionalScope(location.ProjectID, region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -558,10 +532,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: email, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -577,10 +547,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: zoneName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -609,10 +575,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: templateName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -629,10 +591,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: igmName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-instance_test.go b/sources/gcp/manual/compute-instance_test.go index 2b60d307..f441a8e2 100644 --- a/sources/gcp/manual/compute-instance_test.go +++ b/sources/gcp/manual/compute-instance_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -55,60 +55,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -285,6 +261,105 @@ func TestComputeInstance(t *testing.T) { } }) + // ListCachesNotFoundWithMemoryCache verifies that when List returns 0 items, + // NOTFOUND is cached (for both "*" and a specific scope). We verify caching + // by: (1) calling cache.Lookup and asserting cache hit with NOTFOUND error, + // (2) repeating the List call and asserting the GCP client is not called again (gomock Times(1)). + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mocks.NewMockComputeInstanceClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + // Empty aggregated iterator: one Next() then Done (for List("*")). + mockAggIter := mocks.NewMockInstancesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.InstancesScopedListPair{}, iterator.Done) + + // Empty per-zone iterator: one Next() then Done (for List(scope)). + mockListIter := mocks.NewMockComputeInstanceIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" (wildcard): two List calls --- + // First List("*"): cache miss → listAggregatedStream → AggregatedList (0 items) → NOTFOUND cached. + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + + // Verify NOTFOUND is in the cache for "*". + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*) after first call") + } + if qErr == nil { + t.Fatal("expected cached NOTFOUND error for List(*), got nil") + } + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected cached error type NOTFOUND for List(*), got %v", qErr.GetErrorType()) + } + + // Second List("*"): must hit cache (no second AggregatedList call). + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope: two List calls --- + // First List(scope): cache miss → per-zone List → List (0 items) → NOTFOUND cached. + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + + // Verify NOTFOUND is in the cache for scope. + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope) after first call") + } + if qErr == nil { + t.Fatal("expected cached NOTFOUND error for List(scope), got nil") + } + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected cached error type NOTFOUND for List(scope), got %v", qErr.GetErrorType()) + } + + // Second List(scope): must hit cache (no second List call). + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + + // We know it was cached: (1) cache.Lookup returned cacheHit true with NOTFOUND, and + // (2) ctrl.Finish() verifies AggregatedList and List were each called exactly once. + }) + t.Run("GetWithInitializeParams", func(t *testing.T) { wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) @@ -329,60 +404,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -393,40 +444,24 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("projects/%s/global/images/test-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, ) @@ -468,60 +503,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -531,10 +542,6 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -575,60 +582,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -638,10 +621,6 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -678,60 +657,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -741,10 +696,6 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: serviceAccountEmail, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -791,60 +742,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -855,20 +782,12 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceTemplateName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeInstanceGroupManager.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: igmName, ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, ) @@ -910,60 +829,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -974,10 +869,6 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceTemplateName, ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, ) diff --git a/sources/gcp/manual/compute-instant-snapshot.go b/sources/gcp/manual/compute-instant-snapshot.go index 60b8ce64..fb9d71e4 100644 --- a/sources/gcp/manual/compute-instant-snapshot.go +++ b/sources/gcp/manual/compute-instant-snapshot.go @@ -3,15 +3,16 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -125,6 +126,8 @@ func (c computeInstantSnapshotWrapper) ListStream(ctx context.Context, stream di Zone: location.Zone, }) + var itemsSent int + var hadError bool for { instantSnapshot, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -138,11 +141,24 @@ func (c computeInstantSnapshotWrapper) ListStream(ctx context.Context, stream di item, sdpErr := c.gcpComputeInstantSnapshotToSDPItem(ctx, instantSnapshot, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instant snapshots found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -153,6 +169,8 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -168,6 +186,7 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -188,11 +207,13 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, item, sdpErr := c.gcpComputeInstantSnapshotToSDPItem(ctx, instantSnapshot, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -203,6 +224,17 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instant snapshots found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeInstantSnapshotWrapper) gcpComputeInstantSnapshotToSDPItem(ctx context.Context, instantSnapshot *computepb.InstantSnapshot, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -235,10 +267,6 @@ func (c computeInstantSnapshotWrapper) gcpComputeInstantSnapshotToSDPItem(ctx co Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/compute-instant-snapshot_test.go b/sources/gcp/manual/compute-instant-snapshot_test.go index f460edc6..62813897 100644 --- a/sources/gcp/manual/compute-instant-snapshot_test.go +++ b/sources/gcp/manual/compute-instant-snapshot_test.go @@ -5,14 +5,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -75,10 +76,6 @@ func TestComputeInstantSnapshot(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } @@ -242,6 +239,77 @@ func TestComputeInstantSnapshot(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeInstantSnapshotsClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockInstantSnapshotsScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.InstantSnapshotsScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeInstantSnapshotIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeInstantSnapshot(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeInstantSnapshot(snapshotName, zone string, status computepb.InstantSnapshot_Status) *computepb.InstantSnapshot { diff --git a/sources/gcp/manual/compute-machine-image.go b/sources/gcp/manual/compute-machine-image.go index bec7063a..4c9343d9 100644 --- a/sources/gcp/manual/compute-machine-image.go +++ b/sources/gcp/manual/compute-machine-image.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -120,6 +120,8 @@ func (c computeMachineImageWrapper) ListStream(ctx context.Context, stream disco Project: location.ProjectID, }) + var itemsSent int + var hadError bool for { machineImage, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -133,11 +135,24 @@ func (c computeMachineImageWrapper) ListStream(ctx context.Context, stream disco item, sdpErr := c.gcpComputeMachineImageToSDPItem(ctx, machineImage, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute machine images found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -172,10 +187,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: networkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -193,10 +204,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: subnetworkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -214,10 +221,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: networkAttachmentName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -231,10 +234,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: networkIP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -246,10 +245,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -262,10 +257,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: natIP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -279,10 +270,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: externalIpv6, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -302,10 +289,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) if sourceDiskEncryptionKey := disk.GetDiskEncryptionKey(); sourceDiskEncryptionKey != nil { @@ -330,10 +313,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: sourceImage, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -351,10 +330,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: snapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -384,10 +359,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: saEmail, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -406,10 +377,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: acceleratorTypeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -433,10 +400,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: sourceInstanceName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -455,10 +418,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -496,10 +455,6 @@ func (c computeMachineImageWrapper) addKMSKeyLink(sdpItem *sdp.Item, keyName str Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-machine-image_test.go b/sources/gcp/manual/compute-machine-image_test.go index 57b3f1c9..7aac8391 100644 --- a/sources/gcp/manual/compute-machine-image_test.go +++ b/sources/gcp/manual/compute-machine-image_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -53,10 +53,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -64,10 +60,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Network Attachment link { @@ -75,10 +67,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network-attachment", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IPv4 internal IP address { @@ -86,10 +74,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // IPv6 internal address { @@ -97,10 +81,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:db8::1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // External IPv4 address (NAT IP) { @@ -108,10 +88,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // External IPv6 address { @@ -119,10 +95,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:db8::2", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Disk source link { @@ -130,10 +102,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Disk encryption key { @@ -141,10 +109,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source image link (SEARCH handles full URI) { @@ -152,10 +116,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://www.googleapis.com/compute/v1/projects/test-project-id/global/images/test-source-image", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source snapshot link { @@ -163,10 +123,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source image encryption key { @@ -174,10 +130,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source snapshot encryption key { @@ -185,10 +137,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account link { @@ -196,10 +144,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project-id.iam.gserviceaccount.com", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Accelerator type link { @@ -207,10 +151,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nvidia-tesla-k80", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Machine image encryption key { @@ -218,10 +158,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-machine-encryption-key", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source instance link { @@ -229,10 +165,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Saved disk link (from savedDisks) { @@ -240,10 +172,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-saved-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -400,6 +328,47 @@ func TestComputeMachineImage(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeMachineImageClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockComputeMachineImageIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeMachineImage(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeMachineImage(imageName string, status computepb.MachineImage_Status) *computepb.MachineImage { diff --git a/sources/gcp/manual/compute-node-group.go b/sources/gcp/manual/compute-node-group.go index b05239aa..cc6b6960 100644 --- a/sources/gcp/manual/compute-node-group.go +++ b/sources/gcp/manual/compute-node-group.go @@ -3,6 +3,7 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -10,9 +11,9 @@ import ( "google.golang.org/protobuf/proto" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -141,6 +142,8 @@ func (c computeNodeGroupWrapper) ListStream(ctx context.Context, stream discover Zone: location.Zone, }) + var itemsSent int + var hadError bool for { nodeGroup, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -154,11 +157,24 @@ func (c computeNodeGroupWrapper) ListStream(ctx context.Context, stream discover item, sdpErr := c.gcpComputeNodeGroupToSDPItem(ctx, nodeGroup, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute node groups found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -169,6 +185,8 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -184,6 +202,7 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -204,11 +223,13 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea item, sdpErr := c.gcpComputeNodeGroupToSDPItem(ctx, nodeGroup, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -219,6 +240,17 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute node groups found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeNodeGroupWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { @@ -298,10 +330,6 @@ func (c computeNodeGroupWrapper) gcpComputeNodeGroupToSDPItem(ctx context.Contex Query: name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-node-group_test.go b/sources/gcp/manual/compute-node-group_test.go index 81c2d901..e099d5f3 100644 --- a/sources/gcp/manual/compute-node-group_test.go +++ b/sources/gcp/manual/compute-node-group_test.go @@ -6,14 +6,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -52,10 +53,6 @@ func TestComputeNodeGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "node-template-1", ExpectedScope: "test-project.northamerica-northeast1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -196,6 +193,77 @@ func TestComputeNodeGroup(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeNodeGroupClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockNodeGroupsScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.NodeGroupsScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeNodeGroupIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeNodeGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + t.Run("Search", func(t *testing.T) { wrapper := manual.NewComputeNodeGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -264,6 +332,51 @@ func TestComputeNodeGroup(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeNodeGroupClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + query := "https://www.googleapis.com/compute/v1/projects/cache-test-project/zones/us-central1-a/nodeTemplates/nonexistent-template" + + mockIter := mocks.NewMockComputeNodeGroupIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeNodeGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("SearchStream", func(t *testing.T) { wrapper := manual.NewComputeNodeGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/compute-node-template.go b/sources/gcp/manual/compute-node-template.go index a862fae9..34b40b17 100644 --- a/sources/gcp/manual/compute-node-template.go +++ b/sources/gcp/manual/compute-node-template.go @@ -3,15 +3,16 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -125,6 +126,8 @@ func (c computeNodeTemplateWrapper) ListStream(ctx context.Context, stream disco Region: location.Region, }) + var itemsSent int + var hadError bool for { nodeTemplate, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -138,11 +141,24 @@ func (c computeNodeTemplateWrapper) ListStream(ctx context.Context, stream disco item, sdpErr := c.gcpComputeNodeTemplateToSDPItem(nodeTemplate, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute node templates found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -153,6 +169,8 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -168,6 +186,7 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -188,11 +207,13 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st item, sdpErr := c.gcpComputeNodeTemplateToSDPItem(nodeTemplate, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -203,6 +224,17 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute node templates found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeNodeTemplateWrapper) gcpComputeNodeTemplateToSDPItem(nodeTemplate *computepb.NodeTemplate, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -229,10 +261,6 @@ func (c computeNodeTemplateWrapper) gcpComputeNodeTemplateToSDPItem(nodeTemplate Query: nodeTemplate.GetName(), Scope: "*", }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) return sdpItem, nil diff --git a/sources/gcp/manual/compute-node-template_test.go b/sources/gcp/manual/compute-node-template_test.go index 7467652f..e4299916 100644 --- a/sources/gcp/manual/compute-node-template_test.go +++ b/sources/gcp/manual/compute-node-template_test.go @@ -5,14 +5,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -90,10 +91,6 @@ func TestComputeNodeTemplate(t *testing.T) { ExpectedScope: "*", // [SPEC] The node groups does not affect the node template. - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } @@ -193,6 +190,77 @@ func TestComputeNodeTemplate(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeNodeTemplateClient(ctrl) + projectID := "cache-test-project" + region := "us-central1" + scope := projectID + "." + region + + mockAggIter := mocks.NewMockNodeTemplatesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.NodeTemplatesScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeNodeTemplateIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeNodeTemplate(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } // Create an node template fixture (as returned from GCP API). diff --git a/sources/gcp/manual/compute-region-instance-group-manager.go b/sources/gcp/manual/compute-region-instance-group-manager.go index ed159890..02221d8a 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager.go +++ b/sources/gcp/manual/compute-region-instance-group-manager.go @@ -3,14 +3,15 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -137,6 +138,8 @@ func (c computeRegionInstanceGroupManagerWrapper) ListStream(ctx context.Context Region: location.Region, }) + var itemsSent int + var hadError bool for { igm, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -150,17 +153,32 @@ func (c computeRegionInstanceGroupManagerWrapper) ListStream(ctx context.Context item, sdpErr := c.gcpRegionInstanceGroupManagerToSDPItem(ctx, igm, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute region instance group managers found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } func (c computeRegionInstanceGroupManagerWrapper) listAllRegionsStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey) { // Use a pool to list across all regions in parallel p := pool.New().WithContext(ctx).WithMaxGoroutines(10) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, location := range c.Locations() { p.Go(func(ctx context.Context) error { @@ -176,17 +194,20 @@ func (c computeRegionInstanceGroupManagerWrapper) listAllRegionsStream(ctx conte } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, location.ToScope(), c.Type())) + hadError.Store(true) return iterErr } item, sdpErr := c.gcpRegionInstanceGroupManagerToSDPItem(ctx, igm, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } return nil @@ -195,6 +216,17 @@ func (c computeRegionInstanceGroupManagerWrapper) listAllRegionsStream(ctx conte // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute region instance group managers found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeRegionInstanceGroupManagerWrapper) gcpRegionInstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *computepb.InstanceGroupManager, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-region-instance-group-manager_test.go b/sources/gcp/manual/compute-region-instance-group-manager_test.go index 582751cd..2e90abaa 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-region-instance-group-manager_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -65,60 +65,36 @@ func TestComputeRegionInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "unit-test-template", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeAutoscaler.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-autoscaler", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -143,60 +119,36 @@ func TestComputeRegionInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "regional-template", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeAutoscaler.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-autoscaler", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -287,6 +239,77 @@ func TestComputeRegionInstanceGroupManager(t *testing.T) { t.Fatalf("Expected 2 items, got: %d", len(items)) } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockRegionInstanceGroupManagerClient(ctrl) + projectID := "cache-test-project" + region := "us-central1" + scope := projectID + "." + region + + // "*" path calls List once per region; specific scope calls List once. With 1 region: 2 List calls total. + mockIter1 := mocks.NewMockRegionInstanceGroupManagerIterator(ctrl) + mockIter1.EXPECT().Next().Return(nil, iterator.Done) + mockIter2 := mocks.NewMockRegionInstanceGroupManagerIterator(ctrl) + mockIter2.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockIter1).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockIter2).Times(1) + + wrapper := manual.NewComputeRegionInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createRegionInstanceGroupManager(name string, isStable bool, instanceTemplate string) *computepb.InstanceGroupManager { diff --git a/sources/gcp/manual/compute-reservation.go b/sources/gcp/manual/compute-reservation.go index 08f6ac82..b29260a8 100644 --- a/sources/gcp/manual/compute-reservation.go +++ b/sources/gcp/manual/compute-reservation.go @@ -3,15 +3,16 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -127,6 +128,8 @@ func (c computeReservationWrapper) ListStream(ctx context.Context, stream discov Zone: location.Zone, }) + var itemsSent int + var hadError bool for { reservation, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -140,11 +143,24 @@ func (c computeReservationWrapper) ListStream(ctx context.Context, stream discov item, sdpErr := c.gcpComputeReservationToSDPItem(ctx, reservation, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute reservations found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -155,6 +171,8 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -170,6 +188,7 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -190,11 +209,13 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str item, sdpErr := c.gcpComputeReservationToSDPItem(ctx, reservation, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -205,6 +226,17 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute reservations found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeReservationWrapper) gcpComputeReservationToSDPItem(ctx context.Context, reservation *computepb.Reservation, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { @@ -236,10 +268,6 @@ func (c computeReservationWrapper) gcpComputeReservationToSDPItem(ctx context.Co Query: commitmentName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -261,10 +289,6 @@ func (c computeReservationWrapper) gcpComputeReservationToSDPItem(ctx context.Co Query: acceleratorName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -286,10 +310,6 @@ func (c computeReservationWrapper) gcpComputeReservationToSDPItem(ctx context.Co Query: policyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-reservation_test.go b/sources/gcp/manual/compute-reservation_test.go index 85171276..9e317dbc 100644 --- a/sources/gcp/manual/compute-reservation_test.go +++ b/sources/gcp/manual/compute-reservation_test.go @@ -5,14 +5,15 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -48,30 +49,18 @@ func TestComputeReservation(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-commitment", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeAcceleratorType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nvidia-tesla-k80", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -219,6 +208,77 @@ func TestComputeReservation(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeReservationClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockReservationsScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.ReservationsScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeReservationIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeReservation(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeReservation(reservationName string, status computepb.Reservation_Status) *computepb.Reservation { diff --git a/sources/gcp/manual/compute-security-policy.go b/sources/gcp/manual/compute-security-policy.go index b13c5b57..1ccf1355 100644 --- a/sources/gcp/manual/compute-security-policy.go +++ b/sources/gcp/manual/compute-security-policy.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -109,6 +109,8 @@ func (c computeSecurityPolicyWrapper) ListStream(ctx context.Context, stream dis Project: location.ProjectID, }) + var itemsSent int + var hadError bool for { securityPolicy, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -122,10 +124,23 @@ func (c computeSecurityPolicyWrapper) ListStream(ctx context.Context, stream dis item, sdpErr := c.gcpComputeSecurityPolicyToSDPItem(securityPolicy, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute security policies found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -157,10 +172,6 @@ func (c computeSecurityPolicyWrapper) gcpComputeSecurityPolicyToSDPItem(security Query: shared.CompositeLookupKey(policyName, rulePriority), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } diff --git a/sources/gcp/manual/compute-security-policy_test.go b/sources/gcp/manual/compute-security-policy_test.go index 6000c88b..b93d4db3 100644 --- a/sources/gcp/manual/compute-security-policy_test.go +++ b/sources/gcp/manual/compute-security-policy_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -51,10 +51,6 @@ func TestComputeSecurityPolicy(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy|1000", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } @@ -158,6 +154,47 @@ func TestComputeSecurityPolicy(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeSecurityPolicyClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockComputeSecurityPolicyIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeSecurityPolicy(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeSecurityPolicy(policyName string) *computepb.SecurityPolicy { diff --git a/sources/gcp/manual/compute-snapshot.go b/sources/gcp/manual/compute-snapshot.go index f1581286..124cba90 100644 --- a/sources/gcp/manual/compute-snapshot.go +++ b/sources/gcp/manual/compute-snapshot.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -112,6 +112,8 @@ func (c computeSnapshotWrapper) ListStream(ctx context.Context, stream discovery Project: location.ProjectID, }) + var itemsSent int + var hadError bool for { snapshot, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -125,11 +127,24 @@ func (c computeSnapshotWrapper) ListStream(ctx context.Context, stream discovery item, sdpErr := c.gcpComputeSnapshotToSDPItem(ctx, snapshot, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute snapshots found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -161,10 +176,6 @@ func (c computeSnapshotWrapper) gcpComputeSnapshotToSDPItem(ctx context.Context, Query: licenseName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -182,10 +193,6 @@ func (c computeSnapshotWrapper) gcpComputeSnapshotToSDPItem(ctx context.Context, Query: instantSnapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -208,10 +215,6 @@ func (c computeSnapshotWrapper) gcpComputeSnapshotToSDPItem(ctx context.Context, Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } @@ -234,10 +237,6 @@ func (c computeSnapshotWrapper) gcpComputeSnapshotToSDPItem(ctx context.Context, Query: snapshotSchedulePolicyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -283,10 +282,6 @@ func (c computeSnapshotWrapper) addKMSKeyLink(sdpItem *sdp.Item, keyName string, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-snapshot_test.go b/sources/gcp/manual/compute-snapshot_test.go index 602ba9d9..34b013f8 100644 --- a/sources/gcp/manual/compute-snapshot_test.go +++ b/sources/gcp/manual/compute-snapshot_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -51,67 +51,42 @@ func TestComputeSnapshot(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-license", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstantSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instant-snapshot", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-snapshot-schedule-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -275,6 +250,47 @@ func TestComputeSnapshot(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeSnapshotsClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockComputeSnapshotIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeSnapshot(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeSnapshot(snapshotName string, status computepb.Snapshot_Status) *computepb.Snapshot { diff --git a/sources/gcp/manual/iam-service-account-key.go b/sources/gcp/manual/iam-service-account-key.go index fd18c5be..4ce0dbbc 100644 --- a/sources/gcp/manual/iam-service-account-key.go +++ b/sources/gcp/manual/iam-service-account-key.go @@ -6,9 +6,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -200,13 +200,6 @@ func (c iamServiceAccountKeyWrapper) gcpIAMServiceAccountKeyToSDPItem(key *admin Query: serviceAccountName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // If service account is deleted, all keys that belong to it are deleted - // If key is deleted, resources using that particular key lose access to service-account. - // But account itself keeps working. - In: true, - Out: false, - }, }) return sdpItem, nil diff --git a/sources/gcp/manual/iam-service-account-key_test.go b/sources/gcp/manual/iam-service-account-key_test.go index 253fbd87..ae2c745c 100644 --- a/sources/gcp/manual/iam-service-account-key_test.go +++ b/sources/gcp/manual/iam-service-account-key_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -50,7 +50,6 @@ func TestIAMServiceAccountKey(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: testServiceAccount, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -92,6 +91,48 @@ func TestIAMServiceAccountKey(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockIAMServiceAccountKeyClient(ctrl) + projectID := "cache-test-project" + scope := projectID + query := "nonexistent-sa@cache-test-project.iam.gserviceaccount.com" + + mockClient.EXPECT().Search(ctx, gomock.Any()).Return(&adminpb.ListServiceAccountKeysResponse{Keys: nil}, nil).Times(1) + + wrapper := manual.NewIAMServiceAccountKey(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("SearchWithTerraformQueryMap", func(t *testing.T) { wrapper := manual.NewIAMServiceAccountKey(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) diff --git a/sources/gcp/manual/iam-service-account.go b/sources/gcp/manual/iam-service-account.go index 1390b790..1d5c4f2e 100644 --- a/sources/gcp/manual/iam-service-account.go +++ b/sources/gcp/manual/iam-service-account.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -127,6 +127,8 @@ func (c iamServiceAccountWrapper) ListStream(ctx context.Context, stream discove results := c.client.List(ctx, req) + var itemsSent int + var hadError bool for { sa, iterErr := results.Next() if errors.Is(iterErr, iterator.Done) { @@ -140,11 +142,24 @@ func (c iamServiceAccountWrapper) ListStream(ctx context.Context, stream discove item, sdpErr := c.gcpIAMServiceAccountToSDPItem(sa, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no IAM service accounts found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -172,10 +187,6 @@ func (c iamServiceAccountWrapper) gcpIAMServiceAccountToSDPItem(serviceAccount * Query: projectID, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -190,10 +201,6 @@ func (c iamServiceAccountWrapper) gcpIAMServiceAccountToSDPItem(serviceAccount * Query: serviceAccountID, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/iam-service-account_test.go b/sources/gcp/manual/iam-service-account_test.go index 9f5ab384..08b2caba 100644 --- a/sources/gcp/manual/iam-service-account_test.go +++ b/sources/gcp/manual/iam-service-account_test.go @@ -9,9 +9,9 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -50,14 +50,12 @@ func TestIAMServiceAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-project-id", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }, { ExpectedType: gcpshared.IAMServiceAccountKey.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-service-account-id", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, } @@ -83,14 +81,12 @@ func TestIAMServiceAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-project-id", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }, { ExpectedType: gcpshared.IAMServiceAccountKey.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-service-account-id", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, } @@ -185,6 +181,47 @@ func TestIAMServiceAccount(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockIAMServiceAccountClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockIAMServiceAccountIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewIAMServiceAccount(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } // createServiceAccount creates a ServiceAccount with the specified fields. diff --git a/sources/gcp/manual/logging-sink.go b/sources/gcp/manual/logging-sink.go index eb22d8d6..b98b8726 100644 --- a/sources/gcp/manual/logging-sink.go +++ b/sources/gcp/manual/logging-sink.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -107,6 +107,8 @@ func (l loggingSinkWrapper) ListStream(ctx context.Context, stream discovery.Que Parent: fmt.Sprintf("projects/%s", location.ProjectID), }) + var itemsSent int + var hadError bool for { sink, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -120,11 +122,24 @@ func (l loggingSinkWrapper) ListStream(ctx context.Context, stream discovery.Que item, sdpErr := l.gcpLoggingSinkToItem(sink, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no logging sinks found in scope " + scope, + Scope: scope, + SourceName: l.Name(), + ItemType: l.Type(), + ResponderName: l.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -157,10 +172,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: parts[1], // Bucket name Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Changes to bucket affect sink - Out: false, // Changes to sink don't affect bucket - }, }) } case strings.HasPrefix(sink.GetDestination(), "bigquery.googleapis.com"): @@ -174,10 +185,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: values[1], // Dataset ID Scope: values[0], // Project ID }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Changes to dataset affect sink - Out: false, // Changes to sink don't affect dataset - }, }) } case strings.HasPrefix(sink.GetDestination(), "pubsub.googleapis.com"): @@ -191,10 +198,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: values[1], // Topic ID Scope: values[0], // Project ID }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Changes to topic affect sink - Out: false, // Changes to sink don't affect topic - }, }) } case strings.HasPrefix(sink.GetDestination(), "logging.googleapis.com"): @@ -208,10 +211,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: shared.CompositeLookupKey(values[1], values[2]), // location|bucket_ID Scope: values[0], // Project ID }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Changes to bucket affect sink - Out: false, // Changes to sink don't affect bucket - }, }) } } @@ -238,10 +237,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: writerIdentity, // Service account email Scope: projectID, // Project ID extracted from email }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If the service account is deleted or its permissions are changed: The sink may fail to export logs - Out: false, // Changes to the sink don't affect the service account - }, }) } } diff --git a/sources/gcp/manual/logging-sink_test.go b/sources/gcp/manual/logging-sink_test.go index 655a6548..d106a5e1 100644 --- a/sources/gcp/manual/logging-sink_test.go +++ b/sources/gcp/manual/logging-sink_test.go @@ -10,9 +10,9 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" @@ -43,10 +43,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my_bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { @@ -57,10 +53,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my_dataset", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { @@ -71,10 +63,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my_topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { @@ -85,10 +73,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my_bucket"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -145,10 +129,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my_bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IAM Service Account link from writerIdentity { @@ -156,10 +136,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: writerIdentity, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -259,6 +235,47 @@ func TestNewLoggingSink(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockLoggingConfigClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockLoggingSinkIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().ListSinks(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewLoggingSink(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createLoggingSink(name, destination, writerIdentity string) *loggingpb.LogSink { diff --git a/sources/gcp/manual/storage-bucket-iam-policy.go b/sources/gcp/manual/storage-bucket-iam-policy.go new file mode 100644 index 00000000..3ce013d0 --- /dev/null +++ b/sources/gcp/manual/storage-bucket-iam-policy.go @@ -0,0 +1,373 @@ +package manual + +import ( + "context" + "strings" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/sources" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +// Storage Bucket IAM Policy adapter: one item per bucket representing the bucket's full IAM policy. +// Uses the Storage Bucket getIamPolicy V3 API. All Terraform bucket IAM resources (binding, member, policy) map to this item. +// See: https://cloud.google.com/storage/docs/json_api/v1/buckets/getIamPolicy + +var ( + StorageBucketIAMPolicyLookupByBucket = shared.NewItemTypeLookup("bucket", gcpshared.StorageBucketIAMPolicy) +) + +type storageBucketIAMPolicyWrapper struct { + client gcpshared.StorageBucketIAMPolicyGetter + *gcpshared.ProjectBase +} + +// NewStorageBucketIAMPolicy creates a SearchableWrapper for Storage Bucket IAM policy (one item per bucket). +func NewStorageBucketIAMPolicy(client gcpshared.StorageBucketIAMPolicyGetter, locations []gcpshared.LocationInfo) sources.SearchableWrapper { + return &storageBucketIAMPolicyWrapper{ + client: client, + ProjectBase: gcpshared.NewProjectBase( + locations, + sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, + gcpshared.StorageBucketIAMPolicy, + ), + } +} + +func (w *storageBucketIAMPolicyWrapper) IAMPermissions() []string { + return []string{"storage.buckets.getIamPolicy"} +} + +func (w *storageBucketIAMPolicyWrapper) PredefinedRole() string { + return "overmind_custom_role" +} + +func (w *storageBucketIAMPolicyWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + gcpshared.StorageBucket, + gcpshared.IAMServiceAccount, + gcpshared.IAMRole, + gcpshared.ComputeProject, + stdlib.NetworkDNS, + ) +} + +func (w *storageBucketIAMPolicyWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_binding.bucket", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_member.bucket", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_policy.bucket", + }, + } +} + +func (w *storageBucketIAMPolicyWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + StorageBucketIAMPolicyLookupByBucket, + } +} + +func (w *storageBucketIAMPolicyWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + {StorageBucketIAMPolicyLookupByBucket}, + } +} + +func (w *storageBucketIAMPolicyWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + location, err := w.LocationFromScope(scope) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + } + } + if len(queryParts) < 1 || queryParts[0] == "" { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "GET requires bucket name", + } + } + bucketName := queryParts[0] + + bindings, getErr := w.client.GetBucketIAMPolicy(ctx, bucketName) + if getErr != nil { + return nil, gcpshared.QueryError(getErr, scope, w.Type()) + } + + return w.policyToItem(location, bucketName, bindings) +} + +func (w *storageBucketIAMPolicyWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + location, err := w.LocationFromScope(scope) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + } + } + if len(queryParts) < 1 || queryParts[0] == "" { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "SEARCH requires bucket name", + } + } + bucketName := queryParts[0] + + bindings, getErr := w.client.GetBucketIAMPolicy(ctx, bucketName) + if getErr != nil { + return nil, gcpshared.QueryError(getErr, scope, w.Type()) + } + + item, qErr := w.policyToItem(location, bucketName, bindings) + if qErr != nil { + return nil, qErr + } + return []*sdp.Item{item}, nil +} + +// policyBinding is the serialized shape of one binding in the policy item attributes. +type policyBinding struct { + Role string `json:"role"` + Members []string `json:"members"` + ConditionExpression string `json:"conditionExpression,omitempty"` + ConditionTitle string `json:"conditionTitle,omitempty"` + ConditionDescription string `json:"conditionDescription,omitempty"` +} + +// policyToItem builds one SDP item for the bucket's IAM policy and adds linked item queries from all bindings. +func (w *storageBucketIAMPolicyWrapper) policyToItem(location gcpshared.LocationInfo, bucketName string, bindings []gcpshared.BucketIAMBinding) (*sdp.Item, *sdp.QueryError) { + policyBindings := make([]policyBinding, 0, len(bindings)) + for _, b := range bindings { + policyBindings = append(policyBindings, policyBinding{ + Role: b.Role, + Members: b.Members, + ConditionExpression: b.ConditionExpression, + ConditionTitle: b.ConditionTitle, + ConditionDescription: b.ConditionDescription, + }) + } + + type policyAttrs struct { + Bucket string `json:"bucket"` + Bindings []policyBinding `json:"bindings"` + } + attrs, err := shared.ToAttributesWithExclude(policyAttrs{Bucket: bucketName, Bindings: policyBindings}) + if err != nil { + return nil, gcpshared.QueryError(err, location.ToScope(), w.Type()) + } + if err = attrs.Set("uniqueAttr", bucketName); err != nil { + return nil, gcpshared.QueryError(err, location.ToScope(), w.Type()) + } + + item := &sdp.Item{ + Type: gcpshared.StorageBucketIAMPolicy.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: location.ToScope(), + } + + // Link to StorageBucket (In: true, Out: true) + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.StorageBucket.String(), + Method: sdp.QueryMethod_GET, + Query: bucketName, + Scope: location.ProjectID, + }, + }) + + // Collect unique linked SAs, projects, domains, and custom IAM roles across all bindings. + linkedSAs := make(map[string]string) // email -> projectID + linkedProjects := make(map[string]struct{}) + linkedDomains := make(map[string]struct{}) + linkedRoles := make(map[string]map[string]struct{}) // projectID -> set of roleIDs + + for _, b := range bindings { + // Custom roles are in the form projects/{project}/roles/{roleId}; predefined roles are roles/... + if projectID, roleID := extractCustomRoleProjectAndID(b.Role); projectID != "" && roleID != "" { + if linkedRoles[projectID] == nil { + linkedRoles[projectID] = make(map[string]struct{}) + } + linkedRoles[projectID][roleID] = struct{}{} + } + for _, member := range b.Members { + saEmail := extractServiceAccountEmailFromMember(member) + if saEmail != "" { + projectID := extractProjectFromServiceAccountEmail(saEmail) + if projectID != "" && !isGoogleManagedServiceAccountDomain(projectID) { + linkedSAs[saEmail] = projectID + } + } + projectID := extractProjectIDFromProjectPrincipalMember(member) + if projectID != "" { + linkedProjects[projectID] = struct{}{} + } + domainName := extractDomainFromDomainMember(member) + if domainName != "" { + linkedDomains[domainName] = struct{}{} + } + } + } + + for saEmail, projectID := range linkedSAs { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.IAMServiceAccount.String(), + Method: sdp.QueryMethod_GET, + Query: saEmail, + Scope: projectID, + }, + }) + } + for projectID, roleIDs := range linkedRoles { + for roleID := range roleIDs { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.IAMRole.String(), + Method: sdp.QueryMethod_GET, + Query: roleID, + Scope: projectID, + }, + }) + } + } + for projectID := range linkedProjects { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeProject.String(), + Method: sdp.QueryMethod_GET, + Query: projectID, + Scope: projectID, + }, + }) + } + for domainName := range linkedDomains { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: domainName, + Scope: "global", + }, + }) + } + + return item, nil +} + +// extractCustomRoleProjectAndID parses a custom IAM role reference "projects/{project}/roles/{roleId}" +// and returns (projectID, roleID). For predefined roles (e.g. "roles/storage.objectViewer") returns ("", ""). +func extractCustomRoleProjectAndID(role string) (projectID, roleID string) { + const prefix = "projects/" + const suffix = "/roles/" + if !strings.HasPrefix(role, prefix) || !strings.Contains(role, suffix) { + return "", "" + } + rest := strings.TrimPrefix(role, prefix) + idx := strings.Index(rest, suffix) + if idx == -1 { + return "", "" + } + projectID = rest[:idx] + roleID = rest[idx+len(suffix):] + if projectID == "" || roleID == "" { + return "", "" + } + return projectID, roleID +} + +// extractDomainFromDomainMember returns the domain for "domain:example.com" or +// "deleted:domain:example.com", or "" otherwise. The value is a DNS name. +// For deleted members, any "?uid=..." suffix is stripped so the result is a valid DNS link. +func extractDomainFromDomainMember(member string) string { + var domain string + if strings.HasPrefix(member, "deleted:domain:") { + domain = strings.TrimPrefix(member, "deleted:domain:") + } else if strings.HasPrefix(member, "domain:") { + domain = strings.TrimPrefix(member, "domain:") + } else { + return "" + } + // Deleted domain members can include "?uid=123456789"; strip so link uses the actual domain. + if idx := strings.Index(domain, "?"); idx != -1 { + domain = domain[:idx] + } + return domain +} + +// extractProjectIDFromProjectPrincipalMember returns the project ID for project principal members +// (projectOwner:projectId, projectEditor:projectId, projectViewer:projectId), or "" otherwise. +func extractProjectIDFromProjectPrincipalMember(member string) string { + for _, prefix := range []string{"projectOwner:", "projectEditor:", "projectViewer:"} { + if strings.HasPrefix(member, prefix) { + return strings.TrimPrefix(member, prefix) + } + } + return "" +} + +// extractServiceAccountEmailFromMember returns the email for "serviceAccount:email" or "deleted:serviceAccount:email", or "" if not a service account member. +// For deleted members, any "?uid=..." suffix is stripped so the result is a valid IAMServiceAccount lookup query (email only). +func extractServiceAccountEmailFromMember(member string) string { + var email string + if strings.HasPrefix(member, "deleted:serviceAccount:") { + email = strings.TrimPrefix(member, "deleted:serviceAccount:") + } else if strings.HasPrefix(member, "serviceAccount:") { + email = strings.TrimPrefix(member, "serviceAccount:") + } else { + return "" + } + // Deleted SAs can include "?uid=123456789"; strip query part so link uses the actual SA email. + if idx := strings.Index(email, "?"); idx != -1 { + email = email[:idx] + } + return email +} + +// extractProjectFromServiceAccountEmail extracts project ID from "name@project.iam.gserviceaccount.com". +// Only project-scoped SAs use that domain; developer.gserviceaccount.com and appspot.gserviceaccount.com +// use a shared domain where the first label is not a project ID, so we return "" to avoid invalid links. +// For Google-managed SAs (e.g. name@gcp-sa-logging.iam.gserviceaccount.com) use isGoogleManagedServiceAccountDomain to skip. +func extractProjectFromServiceAccountEmail(email string) string { + at := strings.Index(email, "@") + if at == -1 { + return "" + } + domain := email[at+1:] + // Only use first label as project when domain is project.iam.gserviceaccount.com. + // developer.gserviceaccount.com and appspot.gserviceaccount.com must not be treated as project IDs. + if !strings.HasSuffix(domain, ".iam.gserviceaccount.com") { + return "" + } + dot := strings.Index(domain, ".") + if dot == -1 { + return "" + } + return domain[:dot] +} + +// isGoogleManagedServiceAccountDomain reports whether the domain's first label is a known +// Google-managed pattern (not a customer project ID). Such SAs cannot be resolved to a +// project-scoped IAMServiceAccount item with a valid Scope. +func isGoogleManagedServiceAccountDomain(firstLabel string) bool { + // gcp-sa-* (e.g. gcp-sa-logging, gcp-sa-datalabeling) + if strings.HasPrefix(firstLabel, "gcp-sa-") { + return true + } + // cloudservices.gserviceaccount.com, gs-project-accounts, system.gserviceaccount.com + switch firstLabel { + case "cloudservices", "gs-project-accounts", "system": + return true + } + return false +} diff --git a/sources/gcp/manual/storage-bucket-iam-policy_test.go b/sources/gcp/manual/storage-bucket-iam-policy_test.go new file mode 100644 index 00000000..13263489 --- /dev/null +++ b/sources/gcp/manual/storage-bucket-iam-policy_test.go @@ -0,0 +1,546 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/gcp/manual" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/shared" +) + +// fakeBucketIAMPolicyGetter returns a fixed list of bindings for testing. +type fakeBucketIAMPolicyGetter struct { + bindings []gcpshared.BucketIAMBinding + returnErr error + bucketSeen string +} + +func (f *fakeBucketIAMPolicyGetter) GetBucketIAMPolicy(ctx context.Context, bucketName string) ([]gcpshared.BucketIAMBinding, error) { + f.bucketSeen = bucketName + if f.returnErr != nil { + return nil, f.returnErr + } + return f.bindings, nil +} + +// policyWithBindings builds []BucketIAMBinding from role -> members (no condition). +// For conditional bindings, construct []BucketIAMBinding directly. +func policyWithBindings(bindings map[string][]string) []gcpshared.BucketIAMBinding { + out := make([]gcpshared.BucketIAMBinding, 0, len(bindings)) + for role, members := range bindings { + out = append(out, gcpshared.BucketIAMBinding{Role: role, Members: members, ConditionExpression: ""}) + } + return out +} + +func TestStorageBucketIAMPolicy_Get(t *testing.T) { + ctx := context.Background() + projectID := "test-project" + bucketName := "my-bucket" + role := "roles/storage.objectViewer" + saMember := "serviceAccount:siem-sa@test-project.iam.gserviceaccount.com" + + bindings := policyWithBindings(map[string][]string{ + role: {saMember, "user:alice@example.com"}, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + scope := projectID + sdpItem, qErr := adapter.Get(ctx, scope, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + if sdpItem.GetType() != gcpshared.StorageBucketIAMPolicy.String() { + t.Errorf("type: got %s, want %s", sdpItem.GetType(), gcpshared.StorageBucketIAMPolicy.String()) + } + if getter.bucketSeen != bucketName { + t.Errorf("bucket seen: got %s, want %s", getter.bucketSeen, bucketName) + } + + // Policy item has bucket and bindings attributes + if ua, _ := sdpItem.GetAttributes().Get("uniqueAttr"); ua != bucketName { + t.Errorf("uniqueAttr: got %v, want %s", ua, bucketName) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.StorageBucket.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: bucketName, + ExpectedScope: projectID, + }, + { + ExpectedType: gcpshared.IAMServiceAccount.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "siem-sa@test-project.iam.gserviceaccount.com", + ExpectedScope: projectID, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) +} + +func TestStorageBucketIAMPolicy_Get_ProjectPrincipalMembers_Linked(t *testing.T) { + ctx := context.Background() + projectID := "bucket-project" + bucketName := "my-bucket" + role := "roles/storage.objectViewer" + bindings := policyWithBindings(map[string][]string{ + role: { + "projectOwner:other-project", + "projectEditor:another-project", + "projectViewer:bucket-project", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.StorageBucket.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: bucketName, + ExpectedScope: projectID, + }, + { + ExpectedType: gcpshared.ComputeProject.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "other-project", + ExpectedScope: "other-project", + }, + { + ExpectedType: gcpshared.ComputeProject.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "another-project", + ExpectedScope: "another-project", + }, + { + ExpectedType: gcpshared.ComputeProject.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "bucket-project", + ExpectedScope: "bucket-project", + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) +} + +func TestStorageBucketIAMPolicy_Get_ProjectPrincipalMembers_Deduplicated(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.admin": { + "projectOwner:shared-project", + "projectEditor:shared-project", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var projectLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.ComputeProject.String() { + projectLinks++ + if q.GetQuery().GetQuery() != "shared-project" || q.GetQuery().GetScope() != "shared-project" { + t.Errorf("ComputeProject link: got query=%q scope=%q, want shared-project", q.GetQuery().GetQuery(), q.GetQuery().GetScope()) + } + } + } + if projectLinks != 1 { + t.Errorf("expected 1 ComputeProject link (deduplicated), got %d", projectLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_DeletedServiceAccount_IsLinked(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "deleted:serviceAccount:old-sa@my-project.iam.gserviceaccount.com?uid=123456789", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var iamLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.IAMServiceAccount.String() { + iamLinks++ + if q.GetQuery().GetScope() != "my-project" { + t.Errorf("IAM link scope: got %q, want my-project", q.GetQuery().GetScope()) + } + } + } + if iamLinks != 1 { + t.Errorf("expected 1 IAMServiceAccount link for deleted:serviceAccount: member, got %d", iamLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_DomainMembers_EmitDNSLinks(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "domain:example.com", + "domain:acme.co.uk", + "domain:example.com", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var dnsLinks int + dnsQueries := make(map[string]struct{}) + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == "dns" { + dnsLinks++ + dnsQueries[q.GetQuery().GetQuery()] = struct{}{} + if q.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH || q.GetQuery().GetScope() != "global" { + t.Errorf("dns link: method=%v scope=%q (want SEARCH, global)", q.GetQuery().GetMethod(), q.GetQuery().GetScope()) + } + } + } + if dnsLinks != 2 { + t.Errorf("expected 2 dns links (example.com, acme.co.uk; example.com deduped), got %d", dnsLinks) + } + if _, ok := dnsQueries["example.com"]; !ok { + t.Error("missing dns link for example.com") + } + if _, ok := dnsQueries["acme.co.uk"]; !ok { + t.Error("missing dns link for acme.co.uk") + } +} + +func TestStorageBucketIAMPolicy_Get_DeletedDomainMember_StripsUIDSuffix(t *testing.T) { + // deleted:domain:example.com?uid=123456789 should produce a DNS link with query "example.com", not "example.com?uid=123456789". + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "deleted:domain:example.com?uid=123456789", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var dnsLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == "dns" { + dnsLinks++ + query := q.GetQuery().GetQuery() + if query != "example.com" { + t.Errorf("dns link query: got %q, want example.com (?uid= suffix must be stripped)", query) + } + } + } + if dnsLinks != 1 { + t.Errorf("expected 1 dns link, got %d", dnsLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_CustomRole_EmitsIAMRoleLink(t *testing.T) { + // Bindings that reference custom IAM roles (projects/{project}/roles/{roleId}) should emit LinkedItemQuery to IAMRole. + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := []gcpshared.BucketIAMBinding{ + { + Role: "projects/custom-project/roles/myCustomRole", + Members: []string{"user:admin@example.com"}, + ConditionExpression: "", + ConditionTitle: "", + ConditionDescription: "", + }, + { + Role: "roles/storage.objectViewer", + Members: []string{"user:viewer@example.com"}, + ConditionExpression: "", + ConditionTitle: "", + ConditionDescription: "", + }, + } + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var iamRoleLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.IAMRole.String() { + iamRoleLinks++ + if q.GetQuery().GetScope() != "custom-project" || q.GetQuery().GetQuery() != "myCustomRole" { + t.Errorf("IAMRole link: got scope=%q query=%q, want scope=custom-project query=myCustomRole", q.GetQuery().GetScope(), q.GetQuery().GetQuery()) + } + } + } + if iamRoleLinks != 1 { + t.Errorf("expected 1 IAMRole link for custom role, got %d", iamRoleLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_GoogleManagedSA_SkipsLink(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "serviceAccount:my-sa@my-project.iam.gserviceaccount.com", + "serviceAccount:123456@gcp-sa-logging.iam.gserviceaccount.com", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var iamLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.IAMServiceAccount.String() { + iamLinks++ + if q.GetQuery().GetScope() != "my-project" || q.GetQuery().GetQuery() != "my-sa@my-project.iam.gserviceaccount.com" { + t.Errorf("IAM link: scope=%q query=%q (expected customer SA only)", q.GetQuery().GetScope(), q.GetQuery().GetQuery()) + } + } + } + if iamLinks != 1 { + t.Errorf("expected 1 IAMServiceAccount link (customer SA), got %d (Google-managed SA should be skipped)", iamLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_DeveloperAndAppspotSA_SkipLink(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "serviceAccount:123456@developer.gserviceaccount.com", + "serviceAccount:my-app@appspot.gserviceaccount.com", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var iamLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.IAMServiceAccount.String() { + iamLinks++ + scope := q.GetQuery().GetScope() + if scope == "developer" || scope == "appspot" { + t.Errorf("must not create IAM link with scope %q (not a project ID)", scope) + } + } + } + if iamLinks != 0 { + t.Errorf("expected 0 IAMServiceAccount links for developer/appspot SAs, got %d", iamLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_ClientError(t *testing.T) { + ctx := context.Background() + projectID := "test-project" + getter := &fakeBucketIAMPolicyGetter{returnErr: errors.New("api error"), bindings: nil} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, projectID, "my-bucket", true) + if qErr == nil { + t.Error("expected error when getter returns error") + return + } +} + +func TestStorageBucketIAMPolicy_Search(t *testing.T) { + ctx := context.Background() + projectID := "test-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": {"serviceAccount:sa1@test-project.iam.gserviceaccount.com"}, + "roles/storage.admin": {"user:admin@example.com"}, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("adapter does not implement SearchableAdapter") + return + } + + items, qErr := searchable.Search(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Search failed: %v", qErr) + return + } + + if len(items) != 1 { + t.Errorf("Search: got %d items, want 1 (one policy per bucket)", len(items)) + } + if getter.bucketSeen != bucketName { + t.Errorf("bucket seen: got %s, want %s", getter.bucketSeen, bucketName) + } + + if len(items) > 0 { + if err := items[0].Validate(); err != nil { + t.Errorf("item validation: %v", err) + } + if items[0].GetType() != gcpshared.StorageBucketIAMPolicy.String() { + t.Errorf("Search item type: got %s, want %s", items[0].GetType(), gcpshared.StorageBucketIAMPolicy.String()) + } + } +} + +func TestStorageBucketIAMPolicy_TerraformMapping(t *testing.T) { + bindings := policyWithBindings(map[string][]string{"roles/storage.objectViewer": {"user:u@example.com"}}) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation("p")}) + + mappings := wrapper.TerraformMappings() + wantMaps := map[string]bool{ + "google_storage_bucket_iam_binding.bucket": false, + "google_storage_bucket_iam_member.bucket": false, + "google_storage_bucket_iam_policy.bucket": false, + } + if len(mappings) != 3 { + t.Errorf("TerraformMappings: got %d entries, want 3", len(mappings)) + return + } + for _, m := range mappings { + if m.GetTerraformMethod() != sdp.QueryMethod_GET { + t.Errorf("TerraformMethod: got %v, want GET", m.GetTerraformMethod()) + } + qm := m.GetTerraformQueryMap() + if _, ok := wantMaps[qm]; !ok { + t.Errorf("TerraformQueryMap: unexpected %q", qm) + } + wantMaps[qm] = true + } + for qm, seen := range wantMaps { + if !seen { + t.Errorf("TerraformQueryMap: missing %q", qm) + } + } +} + +func TestStorageBucketIAMPolicy_Get_InsufficientQueryParts(t *testing.T) { + ctx := context.Background() + getter := &fakeBucketIAMPolicyGetter{bindings: nil} + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation("p")}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + // Get with empty query should fail (no bucket name) + _, qErr := adapter.Get(ctx, "p", "", true) + if qErr == nil { + t.Error("expected error when query is empty (no bucket name)") + return + } +} + +func TestStorageBucketIAMPolicy_Get_EmptyPolicy_ReturnsItem(t *testing.T) { + // Bucket with no bindings still returns a valid policy item (empty bindings array). + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + getter := &fakeBucketIAMPolicyGetter{bindings: []gcpshared.BucketIAMBinding{}} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed for empty policy: %v", qErr) + return + } + if sdpItem.GetType() != gcpshared.StorageBucketIAMPolicy.String() { + t.Errorf("type: got %s, want %s", sdpItem.GetType(), gcpshared.StorageBucketIAMPolicy.String()) + } + // Should still link to the bucket + var bucketLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.StorageBucket.String() { + bucketLinks++ + } + } + if bucketLinks != 1 { + t.Errorf("expected 1 StorageBucket link, got %d", bucketLinks) + } +} diff --git a/sources/gcp/proc/proc.go b/sources/gcp/proc/proc.go index 49f8c3b0..588f8e3d 100644 --- a/sources/gcp/proc/proc.go +++ b/sources/gcp/proc/proc.go @@ -19,9 +19,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/api/option" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" _ "github.com/overmindtech/cli/sources/gcp/dynamic/adapters" // Import all adapters to register them "github.com/overmindtech/cli/sources/gcp/manual" diff --git a/sources/gcp/proc/proc_test.go b/sources/gcp/proc/proc_test.go index 33f3baa3..5089058b 100644 --- a/sources/gcp/proc/proc_test.go +++ b/sources/gcp/proc/proc_test.go @@ -4,14 +4,15 @@ import ( "context" "fmt" "sort" + "strings" "sync" "sync/atomic" "testing" "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" _ "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "google.golang.org/protobuf/types/known/structpb" @@ -676,3 +677,287 @@ func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > 0 && (s[:len(substr)] == substr || contains(s[1:], substr)))) } + +// TestCriticalTerraformMappingsRegistered verifies that customer-critical Terraform +// resource types are correctly registered in the adapter metadata. This test mirrors +// the mapping table construction in cli/tfutils/plan_mapper.go — it loads all +// registered adapter metadata, parses TerraformQueryMap entries, and checks that +// each critical Terraform type resolves to the expected Overmind item type. +// +// If this test fails, the affected Terraform resources will show as "Unsupported" +// (skipped) in the change analysis UI, meaning no blast radius or risk analysis. +func TestCriticalTerraformMappingsRegistered(t *testing.T) { + // Build the mapping table from all registered adapter metadata, exactly as + // cli/tfutils/plan_mapper.go does at lines 168-190 + type tfMapEntry struct { + overmindType string + method sdp.QueryMethod + queryField string + } + mappings := make(map[string][]tfMapEntry) + for _, metadata := range Metadata.AllAdapterMetadata() { + if metadata.GetType() == "" { + continue + } + for _, mapping := range metadata.GetTerraformMappings() { + subs := strings.SplitN(mapping.GetTerraformQueryMap(), ".", 2) + if len(subs) != 2 { + continue + } + terraformType := subs[0] + mappings[terraformType] = append(mappings[terraformType], tfMapEntry{ + overmindType: metadata.GetType(), + method: mapping.GetTerraformMethod(), + queryField: subs[1], + }) + } + } + + // Each entry defines a Terraform resource type that must be mapped, what + // Overmind type it should resolve to, and which attribute is extracted from + // the Terraform plan to perform the lookup. + criticalMappings := []struct { + terraformType string + expectedType string + expectedField string + expectedMethod sdp.QueryMethod + reason string // documents why this mapping is critical + }{ + // Core resource mappings + { + terraformType: "google_compute_instance", + expectedType: gcpshared.ComputeInstance.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Core compute resource — one of the most common GCP resources in Terraform", + }, + { + terraformType: "google_compute_network", + expectedType: gcpshared.ComputeNetwork.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "VPC networks are foundational infrastructure with wide blast radius", + }, + { + terraformType: "google_compute_subnetwork", + expectedType: gcpshared.ComputeSubnetwork.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Subnets are critical networking resources", + }, + { + terraformType: "google_storage_bucket", + expectedType: gcpshared.StorageBucket.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Storage buckets are one of the most common GCP resources", + }, + { + terraformType: "google_pubsub_topic", + expectedType: gcpshared.PubSubTopic.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Pub/Sub topics are critical messaging infrastructure", + }, + { + terraformType: "google_pubsub_subscription", + expectedType: gcpshared.PubSubSubscription.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Pub/Sub subscriptions are critical messaging infrastructure", + }, + // Previously broken mappings (fixed in PRs #3755 and #3782) + { + terraformType: "google_compute_region_instance_group_manager", + expectedType: gcpshared.ComputeRegionInstanceGroupManager.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Regional MIG — was missing before PR #3755; customer-reported issue", + }, + { + terraformType: "google_kms_crypto_key", + expectedType: gcpshared.CloudKMSCryptoKey.String(), + expectedField: "id", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "KMS key — TerraformMappings() returned nil before PR #3782; customer-reported issue", + }, + // IAM binding mappings — these Terraform-only resources don't have + // standalone GCP APIs, so they resolve to the parent resource for blast + // radius analysis. + // + // Pub/Sub Subscription IAM + { + terraformType: "google_pubsub_subscription_iam_binding", + expectedType: gcpshared.PubSubSubscription.String(), + expectedField: "subscription", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM binding on subscription — resolves to parent subscription for blast radius", + }, + { + terraformType: "google_pubsub_subscription_iam_member", + expectedType: gcpshared.PubSubSubscription.String(), + expectedField: "subscription", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM member on subscription — resolves to parent subscription for blast radius", + }, + { + terraformType: "google_pubsub_subscription_iam_policy", + expectedType: gcpshared.PubSubSubscription.String(), + expectedField: "subscription", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM policy on subscription — resolves to parent subscription for blast radius", + }, + // Pub/Sub Topic IAM + { + terraformType: "google_pubsub_topic_iam_binding", + expectedType: gcpshared.PubSubTopic.String(), + expectedField: "topic", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM binding on topic — resolves to parent topic for blast radius", + }, + { + terraformType: "google_pubsub_topic_iam_member", + expectedType: gcpshared.PubSubTopic.String(), + expectedField: "topic", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM member on topic — resolves to parent topic for blast radius", + }, + { + terraformType: "google_pubsub_topic_iam_policy", + expectedType: gcpshared.PubSubTopic.String(), + expectedField: "topic", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM policy on topic — resolves to parent topic for blast radius", + }, + // BigQuery Dataset IAM + { + terraformType: "google_bigquery_dataset_iam_binding", + expectedType: gcpshared.BigQueryDataset.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM binding on dataset — resolves to parent dataset for blast radius", + }, + { + terraformType: "google_bigquery_dataset_iam_member", + expectedType: gcpshared.BigQueryDataset.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM member on dataset — resolves to parent dataset for blast radius", + }, + { + terraformType: "google_bigquery_dataset_iam_policy", + expectedType: gcpshared.BigQueryDataset.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM policy on dataset — resolves to parent dataset for blast radius", + }, + // BigQuery Table IAM — resolves via dataset_id (bare table_id would be + // misinterpreted as a dataset ID by the SEARCH handler) + { + terraformType: "google_bigquery_table_iam_binding", + expectedType: gcpshared.BigQueryTable.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM binding on table — resolves via dataset_id to list tables in affected dataset", + }, + { + terraformType: "google_bigquery_table_iam_member", + expectedType: gcpshared.BigQueryTable.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM member on table — resolves via dataset_id to list tables in affected dataset", + }, + { + terraformType: "google_bigquery_table_iam_policy", + expectedType: gcpshared.BigQueryTable.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM policy on table — resolves via dataset_id to list tables in affected dataset", + }, + // Bigtable Instance IAM + { + terraformType: "google_bigtable_instance_iam_binding", + expectedType: gcpshared.BigTableAdminInstance.String(), + expectedField: "instance", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM binding on instance — resolves to parent instance for blast radius", + }, + { + terraformType: "google_bigtable_instance_iam_member", + expectedType: gcpshared.BigTableAdminInstance.String(), + expectedField: "instance", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM member on instance — resolves to parent instance for blast radius", + }, + { + terraformType: "google_bigtable_instance_iam_policy", + expectedType: gcpshared.BigTableAdminInstance.String(), + expectedField: "instance", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM policy on instance — resolves to parent instance for blast radius", + }, + // Bigtable Table IAM — resolves via instance_name (the table attribute is + // a bare name that the SEARCH handler would misinterpret as an instance name) + { + terraformType: "google_bigtable_table_iam_binding", + expectedType: gcpshared.BigTableAdminTable.String(), + expectedField: "instance_name", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM binding on table — resolves via instance_name to list tables in affected instance", + }, + { + terraformType: "google_bigtable_table_iam_member", + expectedType: gcpshared.BigTableAdminTable.String(), + expectedField: "instance_name", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM member on table — resolves via instance_name to list tables in affected instance", + }, + { + terraformType: "google_bigtable_table_iam_policy", + expectedType: gcpshared.BigTableAdminTable.String(), + expectedField: "instance_name", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM policy on table — resolves via instance_name to list tables in affected instance", + }, + } + + for _, tc := range criticalMappings { + t.Run(tc.terraformType, func(t *testing.T) { + entries, ok := mappings[tc.terraformType] + if !ok { + t.Fatalf("Terraform type %q is NOT registered in any adapter metadata. "+ + "This means it will show as 'Unsupported' in change analysis. Reason it's critical: %s", + tc.terraformType, tc.reason) + } + + // Verify at least one mapping resolves to the expected Overmind type + found := false + for _, entry := range entries { + if entry.overmindType == tc.expectedType { + found = true + + if entry.queryField != tc.expectedField { + t.Errorf("Terraform type %q maps to %q but uses query field %q, expected %q", + tc.terraformType, tc.expectedType, entry.queryField, tc.expectedField) + } + + if entry.method != tc.expectedMethod { + t.Errorf("Terraform type %q maps to %q but uses method %s, expected %s", + tc.terraformType, tc.expectedType, entry.method, tc.expectedMethod) + } + break + } + } + + if !found { + actualTypes := make([]string, 0, len(entries)) + for _, e := range entries { + actualTypes = append(actualTypes, e.overmindType) + } + t.Errorf("Terraform type %q is registered but resolves to %v, expected %q. "+ + "Reason: %s", + tc.terraformType, actualTypes, tc.expectedType, tc.reason) + } + }) + } +} diff --git a/sources/gcp/shared/adapter-meta.go b/sources/gcp/shared/adapter-meta.go index fbbf299a..455ad73c 100644 --- a/sources/gcp/shared/adapter-meta.go +++ b/sources/gcp/shared/adapter-meta.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/base.go b/sources/gcp/shared/base.go index 723a1ce9..57e46afa 100644 --- a/sources/gcp/shared/base.go +++ b/sources/gcp/shared/base.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/big-query-clients.go b/sources/gcp/shared/big-query-clients.go index 4fbe4b4b..79308a9e 100644 --- a/sources/gcp/shared/big-query-clients.go +++ b/sources/gcp/shared/big-query-clients.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/bigquery" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" ) type BigQueryRoutineClient interface { @@ -75,7 +75,7 @@ func NewBigQueryRoutineClient(client *bigquery.Client) BigQueryRoutineClient { } } -//go:generate mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/cli/sdp-go +//go:generate mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/cli/go/sdp-go type BigQueryDatasetClient interface { Get(ctx context.Context, projectID, datasetID string) (*bigquery.DatasetMetadata, error) List(ctx context.Context, projectID string, toSDPItem func(ctx context.Context, dataset *bigquery.DatasetMetadata) (*sdp.Item, *sdp.QueryError)) ([]*sdp.Item, *sdp.QueryError) diff --git a/sources/gcp/shared/blast-propagations.go b/sources/gcp/shared/blast-propagations.go deleted file mode 100644 index ee50bcb5..00000000 --- a/sources/gcp/shared/blast-propagations.go +++ /dev/null @@ -1,66 +0,0 @@ -package shared - -import ( - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sources/stdlib" -) - -type Impact struct { - ToSDPItemType shared.ItemType - Description string - BlastPropagation *sdp.BlastPropagation - IsParentToChild bool -} - -var ( - ImpactInOnly = &sdp.BlastPropagation{In: true} - impactBothWays = &sdp.BlastPropagation{In: true, Out: true} -) - -var ( - IPImpactBothWays = &Impact{ - Description: "IP addresses and DNS names are tightly coupled with the source type. The linker automatically detects whether the value is an IP address or DNS name and creates the appropriate link. You can use either stdlib.NetworkIP or stdlib.NetworkDNS in blast propagation - both will automatically detect the actual type.", - ToSDPItemType: stdlib.NetworkIP, - BlastPropagation: impactBothWays, - } - SecurityPolicyImpactInOnly = &Impact{ - Description: "Any change on the security policy impacts the source, but not the other way around.", - ToSDPItemType: ComputeSecurityPolicy, - BlastPropagation: ImpactInOnly, - } - CryptoKeyImpactInOnly = &Impact{ - Description: "If the crypto key is updated: The source may not be able to access encrypted data. If the source is updated: The crypto key remains unaffected.", - ToSDPItemType: CloudKMSCryptoKey, - BlastPropagation: ImpactInOnly, - } - CryptoKeyVersionImpactInOnly = &Impact{ - Description: "If the crypto key version is updated: The source may not be able to access encrypted data. If the source is updated: The crypto key version remains unaffected.", - ToSDPItemType: CloudKMSCryptoKeyVersion, - BlastPropagation: ImpactInOnly, - } - IAMServiceAccountImpactInOnly = &Impact{ - Description: "If the service account is updated: The source may not be able to access encrypted data. If the source is updated: The service account remains unaffected.", - ToSDPItemType: IAMServiceAccount, - BlastPropagation: ImpactInOnly, - } - ResourcePolicyImpactInOnly = &Impact{ - Description: "If the resource policy is updated: The source may not be able to access the resource as expected. If the source is updated: The resource policy remains unaffected.", - ToSDPItemType: ComputeResourcePolicy, - BlastPropagation: ImpactInOnly, - } - ComputeNetworkImpactInOnly = &Impact{ - Description: "If the Compute Network is updated: The source may lose connectivity or fail to run as expected. If the source is updated: The network remains unaffected.", - ToSDPItemType: ComputeNetwork, - BlastPropagation: ImpactInOnly, - } - ComputeSubnetworkImpactInOnly = &Impact{ - Description: "If the Compute Subnetwork is updated: The source may lose connectivity or fail to run as expected. If the source is updated: The subnetwork remains unaffected.", - ToSDPItemType: ComputeSubnetwork, - BlastPropagation: ImpactInOnly, - } -) - -// BlastPropagations maps item types to their blast propagation rules. -// This map is populated during source initiation by individual adapter files. -var BlastPropagations = map[shared.ItemType]map[string]*Impact{} diff --git a/sources/gcp/shared/certificate-manager-clients.go b/sources/gcp/shared/certificate-manager-clients.go new file mode 100644 index 00000000..1aff5b95 --- /dev/null +++ b/sources/gcp/shared/certificate-manager-clients.go @@ -0,0 +1,40 @@ +package shared + +//go:generate mockgen -destination=./mocks/mock_certificate_manager_certificate_client.go -package=mocks -source=certificate-manager-clients.go + +import ( + "context" + + certificatemanager "cloud.google.com/go/certificatemanager/apiv1" + certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" + "github.com/googleapis/gax-go/v2" +) + +// CertificateManagerCertificateClient interface for Certificate Manager Certificate operations +type CertificateManagerCertificateClient interface { + GetCertificate(ctx context.Context, req *certificatemanagerpb.GetCertificateRequest, opts ...gax.CallOption) (*certificatemanagerpb.Certificate, error) + ListCertificates(ctx context.Context, req *certificatemanagerpb.ListCertificatesRequest, opts ...gax.CallOption) CertificateIterator +} + +type CertificateIterator interface { + Next() (*certificatemanagerpb.Certificate, error) +} + +type certificateManagerCertificateClient struct { + client *certificatemanager.Client +} + +func (c *certificateManagerCertificateClient) GetCertificate(ctx context.Context, req *certificatemanagerpb.GetCertificateRequest, opts ...gax.CallOption) (*certificatemanagerpb.Certificate, error) { + return c.client.GetCertificate(ctx, req, opts...) +} + +func (c *certificateManagerCertificateClient) ListCertificates(ctx context.Context, req *certificatemanagerpb.ListCertificatesRequest, opts ...gax.CallOption) CertificateIterator { + return c.client.ListCertificates(ctx, req, opts...) +} + +// NewCertificateManagerCertificateClient creates a new CertificateManagerCertificateClient +func NewCertificateManagerCertificateClient(client *certificatemanager.Client) CertificateManagerCertificateClient { + return &certificateManagerCertificateClient{ + client: client, + } +} diff --git a/sources/gcp/shared/cross_project_linking_test.go b/sources/gcp/shared/cross_project_linking_test.go index bac61bd6..f115e1b8 100644 --- a/sources/gcp/shared/cross_project_linking_test.go +++ b/sources/gcp/shared/cross_project_linking_test.go @@ -4,17 +4,12 @@ import ( "reflect" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestProjectBaseLinkedItemQueryByName_CrossProject verifies that project-level // resources correctly extract the project ID from cross-project URIs func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } - tests := []struct { name string projectID string @@ -34,7 +29,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "my-image", Scope: "my-project", }, - BlastPropagation: blastPropagation, }, }, { @@ -49,7 +43,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "my-image", Scope: "my-project", }, - BlastPropagation: blastPropagation, }, }, { @@ -64,7 +57,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "pcs-clamav-box", Scope: "box-dev-baseos", // Should use extracted project, not context project }, - BlastPropagation: blastPropagation, }, }, { @@ -79,7 +71,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "other-image", Scope: "other-project", // Should use extracted project, not context project }, - BlastPropagation: blastPropagation, }, }, { @@ -101,7 +92,7 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { linkerFunc := ProjectBaseLinkedItemQueryByName(ComputeImage) - got := linkerFunc(tt.projectID, "", tt.query, blastPropagation) + got := linkerFunc(tt.projectID, "", tt.query) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ProjectBaseLinkedItemQueryByName() = %v, want %v\nDescription: %s", got, tt.want, tt.description) } @@ -112,11 +103,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { // TestRegionBaseLinkedItemQueryByName_CrossProject verifies that regional // resources correctly extract the project ID from cross-project URIs func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } - tests := []struct { name string projectID string @@ -138,7 +124,6 @@ func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "my-address", Scope: "my-project.us-central1", }, - BlastPropagation: blastPropagation, }, }, { @@ -154,7 +139,6 @@ func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "other-address", Scope: "other-project.europe-west1", // Should use extracted project, not context project }, - BlastPropagation: blastPropagation, }, }, { @@ -170,7 +154,7 @@ func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { linkerFunc := RegionBaseLinkedItemQueryByName(ComputeAddress) - got := linkerFunc(tt.projectID, tt.fromItemScope, tt.query, blastPropagation) + got := linkerFunc(tt.projectID, tt.fromItemScope, tt.query) if !reflect.DeepEqual(got, tt.want) { t.Errorf("RegionBaseLinkedItemQueryByName() = %v, want %v\nDescription: %s", got, tt.want, tt.description) } @@ -181,11 +165,6 @@ func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { // TestZoneBaseLinkedItemQueryByName_CrossProject verifies that zonal // resources correctly extract the project ID from cross-project URIs func TestZoneBaseLinkedItemQueryByName_CrossProject(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } - tests := []struct { name string projectID string @@ -207,7 +186,6 @@ func TestZoneBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "my-disk", Scope: "my-project.us-central1-a", }, - BlastPropagation: blastPropagation, }, }, { @@ -223,7 +201,6 @@ func TestZoneBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "other-disk", Scope: "other-project.europe-west1-b", // Should use extracted project, not context project }, - BlastPropagation: blastPropagation, }, }, { @@ -239,7 +216,7 @@ func TestZoneBaseLinkedItemQueryByName_CrossProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { linkerFunc := ZoneBaseLinkedItemQueryByName(ComputeDisk) - got := linkerFunc(tt.projectID, tt.fromItemScope, tt.query, blastPropagation) + got := linkerFunc(tt.projectID, tt.fromItemScope, tt.query) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ZoneBaseLinkedItemQueryByName() = %v, want %v\nDescription: %s", got, tt.want, tt.description) } diff --git a/sources/gcp/shared/errors.go b/sources/gcp/shared/errors.go index 6ba4092e..e13f6976 100644 --- a/sources/gcp/shared/errors.go +++ b/sources/gcp/shared/errors.go @@ -4,7 +4,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // QueryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/gcp/shared/item-types.go b/sources/gcp/shared/item-types.go index 434637b0..9b3e6ccb 100644 --- a/sources/gcp/shared/item-types.go +++ b/sources/gcp/shared/item-types.go @@ -66,6 +66,7 @@ var ( StorageBucketAccessControl = shared.NewItemType(GCP, Storage, BucketAccessControl) StorageDefaultObjectAccessControl = shared.NewItemType(GCP, Storage, DefaultObjectAccessControl) StorageNotificationConfig = shared.NewItemType(GCP, Storage, NotificationConfig) + StorageBucketIAMPolicy = shared.NewItemType(GCP, Storage, BucketIAMPolicy) ComputeNetworkAttachment = shared.NewItemType(GCP, Compute, NetworkAttachment) ComputeStoragePool = shared.NewItemType(GCP, Compute, StoragePool) ComputeStoragePoolType = shared.NewItemType(GCP, Compute, StoragePoolType) @@ -173,6 +174,7 @@ var ( ComputeInterconnectAttachment = shared.NewItemType(GCP, Compute, InterconnectAttachment) ComputeServiceAttachment = shared.NewItemType(GCP, Compute, ServiceAttachment) ComputeTargetHttpsProxy = shared.NewItemType(GCP, Compute, TargetHttpsProxy) + ComputeRegionTargetHttpsProxy = shared.NewItemType(GCP, Compute, RegionTargetHttpsProxy) ComputeSSLPolicy = shared.NewItemType(GCP, Compute, SSLPolicy) ComputeTargetHttpProxy = shared.NewItemType(GCP, Compute, TargetHttpProxy) ComputeTargetTcpProxy = shared.NewItemType(GCP, Compute, TargetTcpProxy) @@ -191,6 +193,10 @@ var ( FileInstance = shared.NewItemType(GCP, File, Instance) FileBackup = shared.NewItemType(GCP, File, Backup) CertificateManagerCertificateMap = shared.NewItemType(GCP, CertificateManager, CertificateMap) + CertificateManagerCertificateMapEntry = shared.NewItemType(GCP, CertificateManager, CertificateMapEntry) + CertificateManagerCertificate = shared.NewItemType(GCP, CertificateManager, Certificate) + CertificateManagerDnsAuthorization = shared.NewItemType(GCP, CertificateManager, DnsAuthorization) + CertificateManagerCertificateIssuanceConfig = shared.NewItemType(GCP, CertificateManager, CertificateIssuanceConfig) ComputeRoutePolicy = shared.NewItemType(GCP, Compute, RoutePolicy) // Router Route Policy child resource ComputeBgpRoute = shared.NewItemType(GCP, Compute, BgpRoute) // Router BGP Route child resource NetworkServicesMesh = shared.NewItemType(GCP, NetworkServices, Mesh) // https://cloud.google.com/service-mesh/docs/reference/network-services/rest/v1/projects.locations.meshes/get diff --git a/sources/gcp/shared/kms-asset-loader.go b/sources/gcp/shared/kms-asset-loader.go index af9c99b4..93c481cb 100644 --- a/sources/gcp/shared/kms-asset-loader.go +++ b/sources/gcp/shared/kms-asset-loader.go @@ -15,9 +15,9 @@ import ( "golang.org/x/sync/singleflight" "google.golang.org/protobuf/encoding/protojson" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/shared" ) @@ -463,10 +463,6 @@ func (l *CloudKMSAssetLoader) keyRingLinkedQueries(keyRingVals []string, scope s Query: shared.CompositeLookupKey(keyRingVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to CryptoKeys in this KeyRing @@ -477,10 +473,6 @@ func (l *CloudKMSAssetLoader) keyRingLinkedQueries(keyRingVals []string, scope s Query: shared.CompositeLookupKey(keyRingVals[0], keyRingVals[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) return queries @@ -501,10 +493,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(kmsLocation, keyRing, cryptoKeyName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to parent KeyRing @@ -515,10 +503,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(kmsLocation, keyRing), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) // Link to all CryptoKeyVersions @@ -529,10 +513,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(kmsLocation, keyRing, cryptoKeyName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to primary CryptoKeyVersion if present @@ -547,10 +527,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(keyVersionVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -566,10 +542,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(importJobVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -586,10 +558,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(backendVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -611,10 +579,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyVersionLinkedQueries(values []string, key Query: shared.CompositeLookupKey(values[0], values[1], values[2]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) // Link to ImportJob if present @@ -628,10 +592,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyVersionLinkedQueries(values []string, key Query: shared.CompositeLookupKey(importJobVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -649,10 +609,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyVersionLinkedQueries(values []string, key Query: shared.CompositeLookupKey(ekmVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/shared/link-rules.go b/sources/gcp/shared/link-rules.go new file mode 100644 index 00000000..f03730b0 --- /dev/null +++ b/sources/gcp/shared/link-rules.go @@ -0,0 +1,51 @@ +package shared + +import ( + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +type Impact struct { + ToSDPItemType shared.ItemType + Description string + IsParentToChild bool +} + +var ( + IPImpactBothWays = &Impact{ + Description: "IP addresses and DNS names are tightly coupled with the source type. The linker automatically detects whether the value is an IP address or DNS name and creates the appropriate link. You can use either stdlib.NetworkIP or stdlib.NetworkDNS in the link rules - both will automatically detect the actual type.", + ToSDPItemType: stdlib.NetworkIP, + } + SecurityPolicyImpactInOnly = &Impact{ + Description: "Any change on the security policy impacts the source, but not the other way around.", + ToSDPItemType: ComputeSecurityPolicy, + } + CryptoKeyImpactInOnly = &Impact{ + Description: "If the crypto key is updated: The source may not be able to access encrypted data. If the source is updated: The crypto key remains unaffected.", + ToSDPItemType: CloudKMSCryptoKey, + } + CryptoKeyVersionImpactInOnly = &Impact{ + Description: "If the crypto key version is updated: The source may not be able to access encrypted data. If the source is updated: The crypto key version remains unaffected.", + ToSDPItemType: CloudKMSCryptoKeyVersion, + } + IAMServiceAccountImpactInOnly = &Impact{ + Description: "If the service account is updated: The source may not be able to access encrypted data. If the source is updated: The service account remains unaffected.", + ToSDPItemType: IAMServiceAccount, + } + ResourcePolicyImpactInOnly = &Impact{ + Description: "If the resource policy is updated: The source may not be able to access the resource as expected. If the source is updated: The resource policy remains unaffected.", + ToSDPItemType: ComputeResourcePolicy, + } + ComputeNetworkImpactInOnly = &Impact{ + Description: "If the Compute Network is updated: The source may lose connectivity or fail to run as expected. If the source is updated: The network remains unaffected.", + ToSDPItemType: ComputeNetwork, + } + ComputeSubnetworkImpactInOnly = &Impact{ + Description: "If the Compute Subnetwork is updated: The source may lose connectivity or fail to run as expected. If the source is updated: The subnetwork remains unaffected.", + ToSDPItemType: ComputeSubnetwork, + } +) + +// LinkRules maps item types to their link rules (attribute key -> target type metadata). +// This map is populated during source initiation by individual adapter files. +var LinkRules = map[shared.ItemType]map[string]*Impact{} diff --git a/sources/gcp/shared/linker.go b/sources/gcp/shared/linker.go index 82c773d0..719640b8 100644 --- a/sources/gcp/shared/linker.go +++ b/sources/gcp/shared/linker.go @@ -12,7 +12,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -32,15 +32,13 @@ type ItemLookup map[string]ItemTypeMeta // Linker is responsible for linking items based on their types and relationships. type Linker struct { sdpAssetTypeToAdapterMeta map[shared.ItemType]AdapterMeta - explicitBlastPropagations map[shared.ItemType]map[string]*Impact - manualAdapterLinker map[shared.ItemType]func(scope, fromItemScope, query string, bp *sdp.BlastPropagation) *sdp.LinkedItemQuery + manualAdapterLinker map[shared.ItemType]func(scope, fromItemScope, query string) *sdp.LinkedItemQuery } // NewLinker creates a new Linker instance with the provided item lookup and predefined mappings. func NewLinker() *Linker { return &Linker{ sdpAssetTypeToAdapterMeta: SDPAssetTypeToAdapterMeta, - explicitBlastPropagations: BlastPropagations, manualAdapterLinker: ManualAdapterLinksByAssetType, } } @@ -63,9 +61,9 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd "ovm.gcp.key": key, } - impacts, ok := l.explicitBlastPropagations[fromSDPItemType] + impacts, ok := LinkRules[fromSDPItemType] if !ok { - log.WithContext(ctx).WithFields(lf).Warnf("there are no blast propagations for the FROM item type") + log.WithContext(ctx).WithFields(lf).Warnf("there are no link rules for the FROM item type") return } @@ -89,14 +87,14 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd if linkFunc, ok := l.manualAdapterLinker[impact.ToSDPItemType]; ok { // Special handling for stdlib.NetworkIP and stdlib.NetworkDNS - detect both IP and DNS // This handles fields like "host" that could contain either an IP address or DNS name - // You can specify either IP or DNS in the blast propagation, and it will automatically + // You can specify either IP or DNS in the link rules, and it will automatically // detect which type the value actually is and create the appropriate link if impact.ToSDPItemType == stdlib.NetworkIP || impact.ToSDPItemType == stdlib.NetworkDNS { - l.linkIPOrDNS(ctx, fromSDPItem, toItemGCPResourceName, impact.BlastPropagation) + l.linkIPOrDNS(ctx, fromSDPItem, toItemGCPResourceName) return } - linkedItemQuery := linkFunc(projectID, fromSDPItem.GetScope(), toItemGCPResourceName, impact.BlastPropagation) + linkedItemQuery := linkFunc(projectID, fromSDPItem.GetScope(), toItemGCPResourceName) if linkedItemQuery == nil { log.WithContext(ctx).WithFields(lf).Warn( "manual adapter linker failed to create a linked item query", @@ -209,14 +207,13 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd Query: query, Scope: scope, }, - BlastPropagation: impact.BlastPropagation, }) } // linkIPOrDNS detects whether the value is an IP address or DNS name and creates // the appropriate linked item query. This is used for fields like "host" that // could contain either type of value. -func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemValue string, blastPropagation *sdp.BlastPropagation) { +func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemValue string) { if toItemValue == "" { return } @@ -230,7 +227,6 @@ func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemV Query: toItemValue, Scope: "global", }, - BlastPropagation: blastPropagation, }) return } @@ -244,14 +240,13 @@ func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemV Query: toItemValue, Scope: "global", }, - BlastPropagation: blastPropagation, }) return } // If neither IP nor DNS, try the manual adapter linker as fallback if linkFunc, ok := l.manualAdapterLinker[stdlib.NetworkIP]; ok { - linkedItemQuery := linkFunc("", fromSDPItem.GetScope(), toItemValue, blastPropagation) + linkedItemQuery := linkFunc("", fromSDPItem.GetScope(), toItemValue) if linkedItemQuery != nil { fromSDPItem.LinkedItemQueries = append( fromSDPItem.LinkedItemQueries, diff --git a/sources/gcp/shared/linker_test.go b/sources/gcp/shared/linker_test.go index 1abbb627..71806580 100644 --- a/sources/gcp/shared/linker_test.go +++ b/sources/gcp/shared/linker_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/manual-adapter-links.go b/sources/gcp/shared/manual-adapter-links.go index fab136b8..d3004001 100644 --- a/sources/gcp/shared/manual-adapter-links.go +++ b/sources/gcp/shared/manual-adapter-links.go @@ -7,14 +7,14 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) -func ZoneBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ZoneBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { + return func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { name := LastPathComponent(query) zone := ExtractPathParam("zones", query) // Extract project ID from URI if present (for cross-project references) @@ -34,7 +34,6 @@ func ZoneBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, from Query: name, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -42,8 +41,8 @@ func ZoneBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, from } } -func RegionBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func RegionBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { + return func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { name := LastPathComponent(query) scope := fromItemScope region := ExtractPathParam("regions", query) @@ -63,7 +62,6 @@ func RegionBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fr Query: name, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -71,8 +69,8 @@ func RegionBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fr } } -func ProjectBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return func(projectID, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ProjectBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, _, query string) *sdp.LinkedItemQuery { + return func(projectID, _, query string) *sdp.LinkedItemQuery { name := LastPathComponent(query) // Extract project ID from URI if present (for cross-project references) extractedProjectID := ExtractPathParam("projects", query) @@ -88,7 +86,6 @@ func ProjectBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, _ Query: name, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -99,7 +96,7 @@ func ProjectBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, _ // ComputeImageLinker handles linking to compute images using SEARCH method. // SEARCH supports any format: full URIs, family names, or specific image names. // The adapter's Search method will intelligently detect the format and use the appropriate API. -func ComputeImageLinker(projectID, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ComputeImageLinker(projectID, _, query string) *sdp.LinkedItemQuery { // Extract project ID from the URI if present, otherwise use the provided projectID imageProjectID := ExtractPathParam("projects", query) if imageProjectID == "" { @@ -116,7 +113,6 @@ func ComputeImageLinker(projectID, _, query string, blastPropagation *sdp.BlastP Query: query, // Pass the full query string so Search can detect the format Scope: imageProjectID, }, - BlastPropagation: blastPropagation, } } @@ -128,7 +124,7 @@ func ComputeImageLinker(projectID, _, query string, blastPropagation *sdp.BlastP // TargetTcpProxy, TargetSslProxy, TargetPool, TargetVpnGateway, TargetInstance, ServiceAttachment). // This function parses the URI to determine the target type and creates the appropriate link. // Supports both full HTTPS URLs and resource name formats. -func ForwardingRuleTargetLinker(projectID, fromItemScope, targetURI string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ForwardingRuleTargetLinker(projectID, fromItemScope, targetURI string) *sdp.LinkedItemQuery { if targetURI == "" { return nil } @@ -221,7 +217,6 @@ func ForwardingRuleTargetLinker(projectID, fromItemScope, targetURI string, blas Query: query, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -232,7 +227,7 @@ func ForwardingRuleTargetLinker(projectID, fromItemScope, targetURI string, blas // The service field can reference either a BackendService (global or regional) or a BackendBucket (global). // This function parses the URI to determine the target type and creates the appropriate link. // Supports both full HTTPS URLs and resource name formats. -func BackendServiceOrBucketLinker(projectID, fromItemScope, backendURI string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func BackendServiceOrBucketLinker(projectID, fromItemScope, backendURI string) *sdp.LinkedItemQuery { if backendURI == "" { return nil } @@ -287,7 +282,6 @@ func BackendServiceOrBucketLinker(projectID, fromItemScope, backendURI string, b Query: query, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -298,7 +292,7 @@ func BackendServiceOrBucketLinker(projectID, fromItemScope, backendURI string, b // Health checks can be either global (project-scoped) or regional (project.region-scoped). // This function parses the URI to determine the scope and creates the appropriate link. // Supports both full HTTPS URLs and resource name formats. -func HealthCheckLinker(projectID, fromItemScope, healthCheckURI string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func HealthCheckLinker(projectID, fromItemScope, healthCheckURI string) *sdp.LinkedItemQuery { if healthCheckURI == "" { return nil } @@ -341,7 +335,6 @@ func HealthCheckLinker(projectID, fromItemScope, healthCheckURI string, blastPro Query: name, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -353,7 +346,7 @@ func HealthCheckLinker(projectID, fromItemScope, healthCheckURI string, blastPro // This can include: forwarding rules (regional/global), instances, target VPN gateways, routers. // This function parses the URI to determine the resource type and creates the appropriate link. // Supports both full HTTPS URLs and resource name formats. -func AddressUsersLinker(ctx context.Context, projectID, userURI string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func AddressUsersLinker(ctx context.Context, projectID, userURI string) *sdp.LinkedItemQuery { if userURI == "" { return nil } @@ -464,15 +457,14 @@ func AddressUsersLinker(ctx context.Context, projectID, userURI string, blastPro Query: query, Scope: scope, }, - BlastPropagation: blastPropagation, } } return nil } -func AWSLinkByARN(awsItem string) func(_, _, arn string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return func(_, _, arn string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func AWSLinkByARN(awsItem string) func(_, _, arn string) *sdp.LinkedItemQuery { + return func(_, _, arn string) *sdp.LinkedItemQuery { // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html#arns-syntax parts := strings.Split(arn, ":") if len(parts) < 5 { @@ -497,7 +489,6 @@ func AWSLinkByARN(awsItem string) func(_, _, arn string, blastPropagation *sdp.B Query: arn, // By default, we search by the full ARN Scope: scope, }, - BlastPropagation: blastPropagation, } } } @@ -507,7 +498,7 @@ func AWSLinkByARN(awsItem string) func(_, _, arn string, blastPropagation *sdp.B // So we need to manually define how to create the linked item query based on the item type and the query string. // // Expects that the query will have all the necessary information to create the linked item query. -var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery{ +var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery{ ComputeInstance: ZoneBaseLinkedItemQueryByName(ComputeInstance), ComputeInstanceGroup: ZoneBaseLinkedItemQueryByName(ComputeInstanceGroup), ComputeInstanceGroupManager: ZoneBaseLinkedItemQueryByName(ComputeInstanceGroupManager), @@ -540,7 +531,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem ComputeTargetInstance: ForwardingRuleTargetLinker, // Service Attachment (regional) - use polymorphic linker ComputeServiceAttachment: ForwardingRuleTargetLinker, - CloudKMSCryptoKeyVersion: func(projectID, _, keyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudKMSCryptoKeyVersion: func(projectID, _, keyName string) *sdp.LinkedItemQuery { location := ExtractPathParam("locations", keyName) keyRing := ExtractPathParam("keyRings", keyName) cryptoKey := ExtractPathParam("cryptoKeys", keyName) @@ -554,7 +545,6 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(location, keyRing, cryptoKey, cryptoKeyVersion), Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil @@ -566,7 +556,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem // The name field can reference projects, folders, or organizations depending on the resource scope. // This function parses the name to determine the target type and creates the appropriate link. // This is registered for CloudResourceManagerProject but can detect and link to all three types. - CloudResourceManagerProject: func(projectID, _, name string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudResourceManagerProject: func(projectID, _, name string) *sdp.LinkedItemQuery { if name == "" { return nil } @@ -581,8 +571,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: projectIDFromName, Scope: projectIDFromName, // Project scope uses project ID as scope }, - BlastPropagation: blastPropagation, - } + } } } else if strings.HasPrefix(name, "folders/") { folderID := ExtractPathParam("folders", name) @@ -594,8 +583,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: folderID, Scope: projectID, // Folder scope uses project ID (may need adjustment when folder adapter is created) }, - BlastPropagation: blastPropagation, - } + } } } else if strings.HasPrefix(name, "organizations/") { orgID := ExtractPathParam("organizations", name) @@ -607,13 +595,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: orgID, Scope: projectID, // Organization scope uses project ID (may need adjustment when org adapter is created) }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - CloudResourceManagerFolder: func(projectID, _, name string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudResourceManagerFolder: func(projectID, _, name string) *sdp.LinkedItemQuery { if name == "" { return nil } @@ -628,13 +615,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: folderID, Scope: projectID, // Folder scope uses project ID (may need adjustment when folder adapter is created) }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - CloudResourceManagerOrganization: func(projectID, _, name string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudResourceManagerOrganization: func(projectID, _, name string) *sdp.LinkedItemQuery { if name == "" { return nil } @@ -649,13 +635,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: orgID, Scope: projectID, // Organization scope uses project ID (may need adjustment when org adapter is created) }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - stdlib.NetworkIP: func(_, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + stdlib.NetworkIP: func(_, _, query string) *sdp.LinkedItemQuery { if query != "" { return &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -664,12 +649,11 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: query, Scope: "global", }, - BlastPropagation: blastPropagation, } } return nil }, - stdlib.NetworkDNS: func(_, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + stdlib.NetworkDNS: func(_, _, query string) *sdp.LinkedItemQuery { if query != "" { return &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -678,12 +662,11 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: query, Scope: "global", }, - BlastPropagation: blastPropagation, } } return nil }, - stdlib.NetworkHTTP: func(_, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + stdlib.NetworkHTTP: func(_, _, query string) *sdp.LinkedItemQuery { if query != "" { // Extract the base URL (remove query parameters and fragments) httpURL := query @@ -702,13 +685,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: httpURL, Scope: "global", }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - CloudKMSCryptoKey: func(projectID, _, keyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudKMSCryptoKey: func(projectID, _, keyName string) *sdp.LinkedItemQuery { //"projects/{kms_project_id}/locations/{region}/keyRings/{key_region}/cryptoKeys/{key} values := ExtractPathParams(keyName, "locations", "keyRings", "cryptoKeys") if len(values) != 3 { @@ -726,12 +708,11 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(location, keyRing, cryptoKey), Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil }, - BigQueryTable: func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + BigQueryTable: func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { if query == "" { return nil } @@ -756,8 +737,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(values[1], values[2]), Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -772,8 +752,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(values[1], values[2]), Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -788,7 +767,6 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(parts[1], parts[2]), Scope: parts[0], }, - BlastPropagation: blastPropagation, } } @@ -798,7 +776,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem aws.KinesisStreamConsumer: AWSLinkByARN("kinesis-stream-consumer"), aws.IAMRole: AWSLinkByARN("iam-role"), aws.MSKCluster: AWSLinkByARN("msk-cluster"), - SQLAdminInstance: func(projectID, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + SQLAdminInstance: func(projectID, _, query string) *sdp.LinkedItemQuery { // Supported formats: // 1) {project}:{location}:{instance} (Cloud Run format) // See: https://cloud.google.com/run/docs/reference/rest/v2/Volume#cloudsqlinstance @@ -815,7 +793,6 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: parts[2], Scope: parts[0], }, - BlastPropagation: blastPropagation, } } @@ -830,8 +807,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -844,13 +820,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: query, Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil }, - BigQueryDataset: func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + BigQueryDataset: func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { // Supported formats: // 1) datasetId (e.g., "my_dataset") // 2) projects/{project}/datasets/{dataset} @@ -908,8 +883,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: dataset, Scope: scope, }, - BlastPropagation: blastPropagation, - } + } } } @@ -924,8 +898,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: parts[1], // dataset ID Scope: parts[0], // project ID }, - BlastPropagation: blastPropagation, - } + } } } @@ -943,12 +916,11 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: query, // dataset ID Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil }, - BigQueryModel: func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + BigQueryModel: func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { // Supported format: // projects/{project}/datasets/{dataset}/models/{model} if query == "" { @@ -970,8 +942,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(dataset, model), Scope: scope, }, - BlastPropagation: blastPropagation, - } + } } } @@ -988,14 +959,13 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(dataset, model), Scope: scope, }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - StorageBucket: func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + StorageBucket: func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { if query == "" { return nil } @@ -1017,8 +987,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -1033,8 +1002,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -1061,25 +1029,42 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: bucketName, Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil }, + // StorageBucketIAMPolicy: link by bucket name using GET (one policy item per bucket). + StorageBucketIAMPolicy: func(projectID, _, query string) *sdp.LinkedItemQuery { + bucketName := query + if idx := strings.Index(query, "/"); idx != -1 { + bucketName = query[:idx] + } + if projectID == "" || bucketName == "" { + return nil + } + return &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: StorageBucketIAMPolicy.String(), + Method: sdp.QueryMethod_GET, + Query: bucketName, + Scope: projectID, + }, + } + }, // OrgPolicyPolicy name field can reference parent project, folder, or organization // This linker is registered for all three parent types since the name field can reference any of them // Format: projects/{project_number}/policies/{constraint} or // folders/{folder_id}/policies/{constraint} or // organizations/{organization_id}/policies/{constraint} - CloudResourceManagerProject: func(projectID, _, policyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return orgPolicyParentLinker(projectID, policyName, blastPropagation) + CloudResourceManagerProject: func(projectID, _, policyName string) *sdp.LinkedItemQuery { + return orgPolicyParentLinker(projectID, policyName) }, - CloudResourceManagerFolder: func(projectID, _, policyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return orgPolicyParentLinker(projectID, policyName, blastPropagation) + CloudResourceManagerFolder: func(projectID, _, policyName string) *sdp.LinkedItemQuery { + return orgPolicyParentLinker(projectID, policyName) }, - CloudResourceManagerOrganization: func(projectID, _, policyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return orgPolicyParentLinker(projectID, policyName, blastPropagation) + CloudResourceManagerOrganization: func(projectID, _, policyName string) *sdp.LinkedItemQuery { + return orgPolicyParentLinker(projectID, policyName) }, } @@ -1092,7 +1077,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem // // It also handles simple project references: projects/{project_id} (without /policies/) // In that case, the scope should be the current project (projectID), not the referenced project. -func orgPolicyParentLinker(projectID, policyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func orgPolicyParentLinker(projectID, policyName string) *sdp.LinkedItemQuery { if policyName == "" { return nil } @@ -1151,7 +1136,6 @@ func orgPolicyParentLinker(projectID, policyName string, blastPropagation *sdp.B Query: parentID, Scope: scope, }, - BlastPropagation: blastPropagation, } } diff --git a/sources/gcp/shared/manual-adapter-links_test.go b/sources/gcp/shared/manual-adapter-links_test.go index 7298b87c..7370f7f9 100644 --- a/sources/gcp/shared/manual-adapter-links_test.go +++ b/sources/gcp/shared/manual-adapter-links_test.go @@ -4,15 +4,14 @@ import ( "reflect" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/stdlib" ) func TestAWSLinkByARN(t *testing.T) { type args struct { - awsItem string - blastPropagation *sdp.BlastPropagation + awsItem string } tests := []struct { @@ -26,10 +25,6 @@ func TestAWSLinkByARN(t *testing.T) { arn: "arn:aws:iam::123456789012:role/MyRole", args: args{ awsItem: "iam-role", - blastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, want: &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -38,10 +33,6 @@ func TestAWSLinkByARN(t *testing.T) { Query: "arn:aws:iam::123456789012:role/MyRole", Scope: "123456789012", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { @@ -49,10 +40,6 @@ func TestAWSLinkByARN(t *testing.T) { arn: "arn:aws:kms:us-west-2:123456789012:key/abcd1234-56ef-78gh-90ij-klmnopqrstuv", args: args{ awsItem: "kms-key", - blastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, want: &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -61,18 +48,13 @@ func TestAWSLinkByARN(t *testing.T) { Query: "arn:aws:kms:us-west-2:123456789012:key/abcd1234-56ef-78gh-90ij-klmnopqrstuv", Scope: "123456789012.us-west-2", // Region scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { name: "Malformed ARN", arn: "invalid-arn", args: args{ - awsItem: "iam-role", - blastPropagation: &sdp.BlastPropagation{}, + awsItem: "iam-role", }, want: nil, }, @@ -80,7 +62,7 @@ func TestAWSLinkByARN(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotFunc := AWSLinkByARN(tt.args.awsItem) - gotLIQ := gotFunc("", "", tt.arn, tt.args.blastPropagation) + gotLIQ := gotFunc("", "", tt.arn) if !reflect.DeepEqual(gotLIQ, tt.want) { t.Errorf("AWSLinkByARN() = %v, want %v", gotLIQ, tt.want) } @@ -90,10 +72,6 @@ func TestAWSLinkByARN(t *testing.T) { func TestForwardingRuleTargetLinker(t *testing.T) { projectID := "test-project" - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: true, - } tests := []struct { name string @@ -111,7 +89,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-http-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -124,7 +101,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-http-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -137,7 +113,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-http-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Global Target HTTPS Proxy tests @@ -151,7 +126,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-https-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -164,7 +138,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-https-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Global Target TCP Proxy tests @@ -178,7 +151,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-tcp-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -191,7 +163,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-tcp-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Global Target SSL Proxy tests @@ -205,7 +176,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-ssl-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -218,7 +188,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-ssl-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Regional Target Pool tests @@ -232,7 +201,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-target-pool", Scope: "test-project.us-central1", }, - BlastPropagation: blastPropagation, }, }, { @@ -245,7 +213,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-target-pool", Scope: "test-project.us-central1", }, - BlastPropagation: blastPropagation, }, }, // Regional Target VPN Gateway tests @@ -259,7 +226,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-vpn-gateway", Scope: "test-project.us-west1", }, - BlastPropagation: blastPropagation, }, }, { @@ -272,7 +238,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-vpn-gateway", Scope: "test-project.us-west1", }, - BlastPropagation: blastPropagation, }, }, // Zonal Target Instance tests @@ -286,7 +251,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-target-instance", Scope: "test-project.us-central1-a", }, - BlastPropagation: blastPropagation, }, }, { @@ -299,7 +263,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-target-instance", Scope: "test-project.us-central1-a", }, - BlastPropagation: blastPropagation, }, }, // Edge cases @@ -326,7 +289,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "targetHttpProxies", // LastPathComponent returns this from trailing slash Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -340,7 +302,7 @@ func TestForwardingRuleTargetLinker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := ForwardingRuleTargetLinker(projectID, "", tt.targetURI, blastPropagation) + got := ForwardingRuleTargetLinker(projectID, "", tt.targetURI) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ForwardingRuleTargetLinker() = %v, want %v", got, tt.want) } @@ -349,11 +311,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { } func TestNetworkDNSLinker(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: true, - } - tests := []struct { name string query string @@ -369,7 +326,6 @@ func TestNetworkDNSLinker(t *testing.T) { Query: "example.com", Scope: "global", }, - BlastPropagation: blastPropagation, }, }, { @@ -382,7 +338,6 @@ func TestNetworkDNSLinker(t *testing.T) { Query: "api.example.com", Scope: "global", }, - BlastPropagation: blastPropagation, }, }, { @@ -399,7 +354,7 @@ func TestNetworkDNSLinker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := linkerFunc("", "", tt.query, blastPropagation) + got := linkerFunc("", "", tt.query) if !reflect.DeepEqual(got, tt.want) { t.Errorf("NetworkDNSLinker() = %v, want %v", got, tt.want) } @@ -408,11 +363,6 @@ func TestNetworkDNSLinker(t *testing.T) { } func TestMSKClusterLinkByARN(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } - tests := []struct { name string arn string @@ -428,7 +378,6 @@ func TestMSKClusterLinkByARN(t *testing.T) { Query: "arn:aws:kafka:us-east-1:123456789012:cluster/my-cluster/abcd1234-abcd-cafe-abab-9876543210ab-4", Scope: "123456789012.us-east-1", }, - BlastPropagation: blastPropagation, }, }, { @@ -441,7 +390,6 @@ func TestMSKClusterLinkByARN(t *testing.T) { Query: "arn:aws:kafka:us-west-2:987654321098:cluster/prod-cluster/efgh5678-efgh-cafe-cdcd-1234567890ab-5", Scope: "987654321098.us-west-2", }, - BlastPropagation: blastPropagation, }, }, { @@ -463,7 +411,7 @@ func TestMSKClusterLinkByARN(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := linkerFunc("", "", tt.arn, blastPropagation) + got := linkerFunc("", "", tt.arn) if !reflect.DeepEqual(got, tt.want) { t.Errorf("MSKClusterLinkByARN() = %v, want %v", got, tt.want) } @@ -473,10 +421,6 @@ func TestMSKClusterLinkByARN(t *testing.T) { func TestHealthCheckLinker(t *testing.T) { projectID := "test-project" - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } tests := []struct { name string @@ -494,7 +438,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-health-check", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -507,7 +450,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-health-check", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -520,7 +462,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-health-check", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Regional Health Check tests @@ -534,7 +475,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-regional-health-check", Scope: "test-project.us-central1", }, - BlastPropagation: blastPropagation, }, }, { @@ -547,7 +487,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-regional-health-check", Scope: "test-project.us-west1", }, - BlastPropagation: blastPropagation, }, }, { @@ -560,7 +499,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "eu-health-check", Scope: "test-project.europe-west1", }, - BlastPropagation: blastPropagation, }, }, // Edge cases @@ -584,14 +522,13 @@ func TestHealthCheckLinker(t *testing.T) { Query: "healthChecks", // LastPathComponent returns this from trailing slash Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := HealthCheckLinker(projectID, "", tt.healthCheckURI, blastPropagation) + got := HealthCheckLinker(projectID, "", tt.healthCheckURI) if !reflect.DeepEqual(got, tt.want) { t.Errorf("HealthCheckLinker() = %v, want %v", got, tt.want) } diff --git a/sources/gcp/shared/mocks/mock_big_query_dataset_client.go b/sources/gcp/shared/mocks/mock_big_query_dataset_client.go index bc5b175f..8e32ea34 100644 --- a/sources/gcp/shared/mocks/mock_big_query_dataset_client.go +++ b/sources/gcp/shared/mocks/mock_big_query_dataset_client.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/cli/sdp-go +// mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/cli/go/sdp-go // // Package mocks is a generated GoMock package. @@ -14,8 +14,8 @@ import ( reflect "reflect" bigquery "cloud.google.com/go/bigquery" - discovery "github.com/overmindtech/cli/discovery" - sdp "github.com/overmindtech/cli/sdp-go" + discovery "github.com/overmindtech/cli/go/discovery" + sdp "github.com/overmindtech/cli/go/sdp-go" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go b/sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go new file mode 100644 index 00000000..e271e814 --- /dev/null +++ b/sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go @@ -0,0 +1,122 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./sources/gcp/shared/certificate-manager-clients.go +// +// Generated by this command: +// +// mockgen -destination=./sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go -package=mocks -source=./sources/gcp/shared/certificate-manager-clients.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" + v2 "github.com/googleapis/gax-go/v2" + shared "github.com/overmindtech/cli/sources/gcp/shared" + gomock "go.uber.org/mock/gomock" +) + +// MockCertificateManagerCertificateClient is a mock of CertificateManagerCertificateClient interface. +type MockCertificateManagerCertificateClient struct { + ctrl *gomock.Controller + recorder *MockCertificateManagerCertificateClientMockRecorder + isgomock struct{} +} + +// MockCertificateManagerCertificateClientMockRecorder is the mock recorder for MockCertificateManagerCertificateClient. +type MockCertificateManagerCertificateClientMockRecorder struct { + mock *MockCertificateManagerCertificateClient +} + +// NewMockCertificateManagerCertificateClient creates a new mock instance. +func NewMockCertificateManagerCertificateClient(ctrl *gomock.Controller) *MockCertificateManagerCertificateClient { + mock := &MockCertificateManagerCertificateClient{ctrl: ctrl} + mock.recorder = &MockCertificateManagerCertificateClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCertificateManagerCertificateClient) EXPECT() *MockCertificateManagerCertificateClientMockRecorder { + return m.recorder +} + +// GetCertificate mocks base method. +func (m *MockCertificateManagerCertificateClient) GetCertificate(ctx context.Context, req *certificatemanagerpb.GetCertificateRequest, opts ...v2.CallOption) (*certificatemanagerpb.Certificate, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetCertificate", varargs...) + ret0, _ := ret[0].(*certificatemanagerpb.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCertificate indicates an expected call of GetCertificate. +func (mr *MockCertificateManagerCertificateClientMockRecorder) GetCertificate(ctx, req any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificate", reflect.TypeOf((*MockCertificateManagerCertificateClient)(nil).GetCertificate), varargs...) +} + +// ListCertificates mocks base method. +func (m *MockCertificateManagerCertificateClient) ListCertificates(ctx context.Context, req *certificatemanagerpb.ListCertificatesRequest, opts ...v2.CallOption) shared.CertificateIterator { + m.ctrl.T.Helper() + varargs := []any{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListCertificates", varargs...) + ret0, _ := ret[0].(shared.CertificateIterator) + return ret0 +} + +// ListCertificates indicates an expected call of ListCertificates. +func (mr *MockCertificateManagerCertificateClientMockRecorder) ListCertificates(ctx, req any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCertificates", reflect.TypeOf((*MockCertificateManagerCertificateClient)(nil).ListCertificates), varargs...) +} + +// MockCertificateIterator is a mock of CertificateIterator interface. +type MockCertificateIterator struct { + ctrl *gomock.Controller + recorder *MockCertificateIteratorMockRecorder + isgomock struct{} +} + +// MockCertificateIteratorMockRecorder is the mock recorder for MockCertificateIterator. +type MockCertificateIteratorMockRecorder struct { + mock *MockCertificateIterator +} + +// NewMockCertificateIterator creates a new mock instance. +func NewMockCertificateIterator(ctrl *gomock.Controller) *MockCertificateIterator { + mock := &MockCertificateIterator{ctrl: ctrl} + mock.recorder = &MockCertificateIteratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCertificateIterator) EXPECT() *MockCertificateIteratorMockRecorder { + return m.recorder +} + +// Next mocks base method. +func (m *MockCertificateIterator) Next() (*certificatemanagerpb.Certificate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Next") + ret0, _ := ret[0].(*certificatemanagerpb.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Next indicates an expected call of Next. +func (mr *MockCertificateIteratorMockRecorder) Next() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockCertificateIterator)(nil).Next)) +} diff --git a/sources/gcp/shared/models.go b/sources/gcp/shared/models.go index b897d26e..337480bd 100644 --- a/sources/gcp/shared/models.go +++ b/sources/gcp/shared/models.go @@ -115,6 +115,7 @@ const ( KeyRing shared.Resource = "key-ring" InstanceSettings shared.Resource = "instance-settings" Bucket shared.Resource = "bucket" + BucketIAMPolicy shared.Resource = "bucket-iam-policy" BucketAccessControl shared.Resource = "bucket-access-control" DefaultObjectAccessControl shared.Resource = "default-object-access-control" NotificationConfig shared.Resource = "storage-notification-config" @@ -194,6 +195,7 @@ const ( InterconnectAttachment shared.Resource = "interconnect-attachment" ServiceAttachment shared.Resource = "service-attachment" TargetHttpsProxy shared.Resource = "target-https-proxy" + RegionTargetHttpsProxy shared.Resource = "region-target-https-proxy" SSLPolicy shared.Resource = "ssl-policy" TargetHttpProxy shared.Resource = "target-http-proxy" TargetTcpProxy shared.Resource = "target-tcp-proxy" @@ -216,6 +218,10 @@ const ( EffectiveSecurityHealthAnalyticsCustomModule shared.Resource = "effective-security-health-analytics-custom-module" // Security Center Management Effective Security Health Analytics Custom Module EffectiveEventThreatDetectionCustomModule shared.Resource = "effective-event-threat-detection-custom-module" // Security Center Management Effective Event Threat Detection Custom Module CertificateMap shared.Resource = "certificate-map" // Certificate Manager Certificate Map + CertificateMapEntry shared.Resource = "certificate-map-entry" // Certificate Manager Certificate Map Entry + Certificate shared.Resource = "certificate" // Certificate Manager Certificate + DnsAuthorization shared.Resource = "dns-authorization" // Certificate Manager DNS Authorization + CertificateIssuanceConfig shared.Resource = "certificate-issuance-config" // Certificate Manager Certificate Issuance Config InternalRange shared.Resource = "internal-range" // Network Connectivity API Internal Range RoutePolicy shared.Resource = "route-policy" // Router Route Policy child resource BgpRoute shared.Resource = "bgp-route" // Router BGP Route child resource diff --git a/sources/gcp/shared/predefined-roles.go b/sources/gcp/shared/predefined-roles.go index e1778af1..f5966f79 100644 --- a/sources/gcp/shared/predefined-roles.go +++ b/sources/gcp/shared/predefined-roles.go @@ -43,14 +43,15 @@ var PredefinedRoles = map[string]role{ }, }, "overmind_custom_role": { - // This is a custom role for Overmind service account with additional BigQuery and Spanner permissions - // It is created in deploy/sources.tf + // Custom role for Overmind with permissions not available in a single least-privilege predefined role. + // Created in deploy/sources.tf. Includes read-only Storage Bucket IAM (getIamPolicy) and BigQuery/Spanner extras. Role: "overmind_custom_role", Link: "deploy/sources.tf", IAMPermissions: []string{ "bigquery.transfers.get", "spanner.databases.get", "spanner.databases.list", + "storage.buckets.getIamPolicy", }, }, "roles/bigquery.metadataViewer": { @@ -83,6 +84,15 @@ var PredefinedRoles = map[string]role{ "bigtable.backups.list", }, }, + "roles/certificatemanager.viewer": { + Role: "roles/certificatemanager.viewer", + // Read-only access to Certificate Manager resources. + Link: "https://cloud.google.com/iam/docs/roles-permissions/certificatemanager#certificatemanager.viewer", + IAMPermissions: []string{ + "certificatemanager.certs.get", + "certificatemanager.certs.list", + }, + }, "roles/cloudfunctions.viewer": { Role: "roles/cloudfunctions.viewer", // Read-only access to functions and locations. diff --git a/sources/gcp/shared/storage-iam.go b/sources/gcp/shared/storage-iam.go new file mode 100644 index 00000000..9796a512 --- /dev/null +++ b/sources/gcp/shared/storage-iam.go @@ -0,0 +1,67 @@ +package shared + +import ( + "context" + "errors" + + "cloud.google.com/go/storage" +) + +// ErrStorageClientNotInitialized is returned when the Storage client was not initialized (e.g. when enumerating adapters without initGCPClients). +var ErrStorageClientNotInitialized = errors.New("storage client not initialized") + +// BucketIAMBinding represents one IAM binding (role + members, optionally with a condition) in a bucket's policy. +// The adapter emits one item per bucket (the full policy); bindings are serialized in that item's bindings array. +type BucketIAMBinding struct { + Role string + Members []string + ConditionExpression string // CEL expression; empty if no condition + ConditionTitle string // optional; empty if no condition or not set + ConditionDescription string // optional; empty if no condition or not set +} + +// StorageBucketIAMPolicyGetter retrieves the IAM policy for a GCS bucket as a slice of bindings. +// See: https://cloud.google.com/storage/docs/json_api/v1/buckets/getIamPolicy +type StorageBucketIAMPolicyGetter interface { + GetBucketIAMPolicy(ctx context.Context, bucketName string) ([]BucketIAMBinding, error) +} + +// storageBucketIAMPolicyGetterImpl implements StorageBucketIAMPolicyGetter using the Storage client. +type storageBucketIAMPolicyGetterImpl struct { + client *storage.Client +} + +// GetBucketIAMPolicy returns the IAM policy for the given bucket. +func (g *storageBucketIAMPolicyGetterImpl) GetBucketIAMPolicy(ctx context.Context, bucketName string) ([]BucketIAMBinding, error) { + if g.client == nil { + return nil, ErrStorageClientNotInitialized + } + policy3, err := g.client.Bucket(bucketName).IAM().V3().Policy(ctx) + if err != nil { + return nil, err + } + out := make([]BucketIAMBinding, 0, len(policy3.Bindings)) + for _, b := range policy3.Bindings { + condExpr := "" + condTitle := "" + condDesc := "" + if b.GetCondition() != nil { + condExpr = b.GetCondition().GetExpression() + condTitle = b.GetCondition().GetTitle() + condDesc = b.GetCondition().GetDescription() + } + out = append(out, BucketIAMBinding{ + Role: b.GetRole(), + Members: b.GetMembers(), + ConditionExpression: condExpr, + ConditionTitle: condTitle, + ConditionDescription: condDesc, + }) + } + return out, nil +} + +// NewStorageBucketIAMPolicyGetter creates a getter that uses the given Storage client. +func NewStorageBucketIAMPolicyGetter(client *storage.Client) StorageBucketIAMPolicyGetter { + return &storageBucketIAMPolicyGetterImpl{client: client} +} diff --git a/sources/gcp/shared/terraform-mappings.go b/sources/gcp/shared/terraform-mappings.go index 0d7b2a79..b7335f9f 100644 --- a/sources/gcp/shared/terraform-mappings.go +++ b/sources/gcp/shared/terraform-mappings.go @@ -1,7 +1,7 @@ package shared import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/shared/base.go b/sources/shared/base.go index 735cd3cd..6789486d 100644 --- a/sources/shared/base.go +++ b/sources/shared/base.go @@ -3,7 +3,7 @@ package shared import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // Base is a struct that holds fundamental pieces for creating an adapter. diff --git a/sources/shared/testing.go b/sources/shared/testing.go index 18c5eac4..342c318f 100644 --- a/sources/shared/testing.go +++ b/sources/shared/testing.go @@ -12,8 +12,8 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" ) // RunStaticTests runs static tests on the given adapter and item. @@ -62,11 +62,10 @@ func ValidateAdapter(t *testing.T, adapter discovery.Adapter) { // QueryTest is a struct that defines the expected properties of a linked item query. type QueryTest struct { - ExpectedType string - ExpectedMethod sdp.QueryMethod - ExpectedQuery string - ExpectedScope string - ExpectedBlastPropagation *sdp.BlastPropagation + ExpectedType string + ExpectedMethod sdp.QueryMethod + ExpectedQuery string + ExpectedScope string } type QueryTests []QueryTest @@ -112,22 +111,6 @@ func (i QueryTests) TestLinkedItems(t *testing.T, item *sdp.Item) { if test.ExpectedMethod != gotLiq.GetQuery().GetMethod() { t.Errorf("for the linked item query %s of %s, expected method %s, got %s", test.ExpectedQuery, test.ExpectedType, test.ExpectedMethod, gotLiq.GetQuery().GetMethod()) } - - if test.ExpectedBlastPropagation == nil { - t.Fatalf("for the linked item query %s of %s, the test case must have a non-nil blast propagation", test.ExpectedQuery, test.ExpectedType) - } - - if gotLiq.GetBlastPropagation() == nil { - t.Fatalf("for the linked item query %s of %s, expected blast propagation to be non-nil", test.ExpectedQuery, test.ExpectedType) - } - - if test.ExpectedBlastPropagation.GetIn() != gotLiq.GetBlastPropagation().GetIn() { - t.Errorf("for the linked item query %s of %s, expected blast propagation [IN] to be %v, got %v", test.ExpectedQuery, test.ExpectedType, test.ExpectedBlastPropagation.GetIn(), gotLiq.GetBlastPropagation().GetIn()) - } - - if test.ExpectedBlastPropagation.GetOut() != gotLiq.GetBlastPropagation().GetOut() { - t.Errorf("for the linked item query %s of %s, expected blast propagation [OUT] to be %v, got %v", test.ExpectedQuery, test.ExpectedType, test.ExpectedBlastPropagation.GetOut(), gotLiq.GetBlastPropagation().GetOut()) - } } } diff --git a/sources/shared/util.go b/sources/shared/util.go index 4baf8289..4f04fa51 100644 --- a/sources/shared/util.go +++ b/sources/shared/util.go @@ -3,7 +3,7 @@ package shared import ( "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // ToAttributesWithExclude converts an interface to SDP attributes using the `sdp.ToAttributesSorted` diff --git a/sources/snapshot/README.md b/sources/snapshot/README.md new file mode 100644 index 00000000..4981e1d7 --- /dev/null +++ b/sources/snapshot/README.md @@ -0,0 +1,169 @@ +# Snapshot Source + +A discovery source that serves items from a snapshot file or URL, enabling local testing with fixed data and deterministic re-runs of v6 investigation jobs. + +## Overview + +The snapshot source loads a protobuf snapshot (`.pb` file) at startup and responds to NATS discovery queries (GET, LIST, SEARCH) with items from that snapshot. This enables: + +- **Local testing**: Run backend services (gateway, api-server, NATS) locally with consistent snapshot data +- **Deterministic v6 re-runs**: Re-run change analysis and blast radius calculations with the same snapshot data +- **Consistent exploration**: Query the same fixed data set repeatedly for debugging and testing + +## Features + +- **Snapshot loading**: Loads snapshots from local files or HTTP(S) URLs +- **Wildcard scope support**: Single adapter handles all types and scopes in the snapshot +- **Full query support**: Implements GET, LIST, and SEARCH query methods +- **In-memory indexing**: Fast lookups by type, scope, GUN, or query string +- **Comprehensive tests**: Unit tests for loader, index, and adapter components + +## Usage + +### Configuration + +The snapshot source requires a snapshot file or URL to be specified: + +**Environment variables:** +- `SNAPSHOT_SOURCE` or `SNAPSHOT_PATH` or `SNAPSHOT_URL` - Path to snapshot file or HTTP(S) URL +- Standard discovery engine config (NATS connection, auth, etc.) + +**Command-line flags:** +```bash +--snapshot-source # Path to snapshot file or URL (required) +--log # Log level (default: info) +--json-log # JSON logging (default: true) +--health-check-port # Health check port (default: 8089) +``` + +### Running Locally + +#### Option 1: With backend services (recommended) + +1. Start backend services (gateway, api-server, NATS) in devcontainer or via docker-compose +2. Run the snapshot source: + +```bash +ALLOW_UNAUTHENTICATED=true \ +SNAPSHOT_SOURCE=/workspace/services/api-server/service/changeanalysis/testdata/snapshot.pb \ +NATS_SERVICE_HOST=nats \ +NATS_SERVICE_PORT=4222 \ +go run ./sources/snapshot/main.go --log=debug --json-log=false +``` + +#### Option 2: Using VS Code launch configuration + +Use the provided launch configurations in `.vscode/launch.json`: + +- **"snapshot-source (with backend)"**: For use when backend services are running +- **"snapshot-source (standalone)"**: For standalone debugging with local NATS + +Update the `SNAPSHOT_SOURCE` environment variable in the launch config to point to your snapshot file. + +#### Option 3: Load snapshot from URL + +```bash +ALLOW_UNAUTHENTICATED=true \ +SNAPSHOT_SOURCE=https://gateway-host/area51/snapshots/{uuid}/protobuf \ +NATS_SERVICE_HOST=nats \ +NATS_SERVICE_PORT=4222 \ +go run ./sources/snapshot/main.go +``` + +### Query Behavior + +The snapshot source implements a **wildcard scope adapter** that handles all types and scopes: + +- **LIST**: Returns all items in the snapshot (or filtered by scope if scope != "*") +- **GET**: Finds an item by its globally unique name (GUN) or unique attribute value +- **SEARCH**: Searches items by regex pattern on globally unique name + +Example queries via the gateway: +``` +LIST *.* # Returns all 179 items in test snapshot +GET *.* # Gets specific item by GUN +SEARCH *.* # Finds items matching pattern +``` + +## Implementation Details + +### Architecture + +``` +sources/snapshot/ +├── main.go # Entrypoint +├── cmd/ +│ └── root.go # Cobra CLI setup, viper config +└── adapters/ + ├── loader.go # Snapshot loading (file/URL) + ├── index.go # In-memory indexing + ├── adapter.go # Discovery adapter implementation + └── main.go # Adapter initialization +``` + +### Snapshot Index + +The source builds in-memory indices for efficient querying: + +- **By GUN**: Map of `GloballyUniqueName` → `*Item` for fast GET lookups +- **By type/scope**: Nested map for filtering by type and scope +- **All items**: Full list for wildcard LIST queries + +### Adapter Strategy + +The snapshot source uses **Option B from the design doc**: a single adapter with wildcard type (`*`) and wildcard scope (`*`). This adapter: + +- Reports `Type() = "*"` and `Scopes() = ["*"]` +- Implements `WildcardScopeAdapter` interface +- Handles all query types (GET, LIST, SEARCH) across all types and scopes in the snapshot + +This differs from "one adapter per (type, scope)" because the gateway's query expansion expects adapters to report specific types. The wildcard approach lets us serve any item from the snapshot regardless of type or scope. + +## Testing + +Run unit tests: +```bash +cd sources/snapshot/adapters +go test -v +``` + +Test snapshot loading: +```bash +cd sources/snapshot +go run main.go --snapshot-source=/path/to/snapshot.pb --help +``` + +Verify with real snapshot: +```bash +cd sources/snapshot +go test -run TestLoadSnapshotFromFile -v ./adapters +``` + +## Example: Using with v6 Investigations + +1. Download a snapshot from Area 51 or use an existing test snapshot +2. Start backend services locally (gateway, api-server, NATS) +3. Start the snapshot source pointing at your snapshot file +4. Run a v6 investigation - it will query from the snapshot instead of live sources +5. Re-run with the same snapshot for consistent, deterministic results + +## Troubleshooting + +**Error: "snapshot has no items"** +- Verify the snapshot file is valid protobuf and contains items +- Check file path or URL is correct + +**Error: "api-key must be set"** +- Set `ALLOW_UNAUTHENTICATED=true` for local testing +- Or provide a valid API key via `API_KEY` env var + +**Error: "could not connect to NATS"** +- Verify NATS is running at the configured host/port +- Check `NATS_SERVICE_HOST` and `NATS_SERVICE_PORT` are correct + +## Related Documentation + +- **Linear issue**: [ENG-2577](https://linear.app/overmind/issue/ENG-2577) +- **Snapshot protobuf**: `sdp/snapshots.proto` +- **Discovery engine**: `go/discovery/` +- **Test snapshot**: `services/api-server/service/changeanalysis/testdata/snapshot.pb` diff --git a/sources/snapshot/adapters/adapter.go b/sources/snapshot/adapters/adapter.go new file mode 100644 index 00000000..06353a98 --- /dev/null +++ b/sources/snapshot/adapters/adapter.go @@ -0,0 +1,149 @@ +package adapters + +import ( + "context" + "fmt" + "regexp" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" +) + +// SnapshotAdapter is a discovery adapter that serves items of a single type +// from a snapshot. One adapter is created per type found in the snapshot so +// that the discovery engine can route specific-type GET/SEARCH queries +// correctly. +type SnapshotAdapter struct { + index *SnapshotIndex + itemType string + scopes []string + metadata *sdp.AdapterMetadata +} + +// NewSnapshotAdapter creates a new per-type adapter backed by the shared index. +func NewSnapshotAdapter(index *SnapshotIndex, itemType string, scopes []string) *SnapshotAdapter { + return &SnapshotAdapter{ + index: index, + itemType: itemType, + scopes: scopes, + metadata: lookupAdapterMetadata(itemType, scopes), + } +} + +func cloneItems(items []*sdp.Item) []*sdp.Item { + out := make([]*sdp.Item, len(items)) + for i, item := range items { + out[i] = proto.Clone(item).(*sdp.Item) + } + return out +} + +func (a *SnapshotAdapter) Type() string { + return a.itemType +} + +func (a *SnapshotAdapter) Name() string { + return fmt.Sprintf("snapshot-%s", a.itemType) +} + +func (a *SnapshotAdapter) Scopes() []string { + return a.scopes +} + +func (a *SnapshotAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { + log.WithFields(log.Fields{ + "scope": scope, + "type": a.itemType, + "query": query, + }).Debug("SnapshotAdapter.Get called") + + // Try GUN lookup first (includes type in the GUN so it's already scoped) + item := a.index.GetByGUN(query) + if item != nil && item.GetType() == a.itemType { + if scope == "*" || item.GetScope() == scope { + return cloneItems([]*sdp.Item{item})[0], nil + } + } + + // Fall back to unique attribute value match within this type + for _, candidateItem := range a.index.GetItemsByTypeAndScope(a.itemType, scope) { + if candidateItem.UniqueAttributeValue() == query { + return cloneItems([]*sdp.Item{candidateItem})[0], nil + } + } + + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("item not found: scope=%s, type=%s, query=%s", scope, a.itemType, query), + Scope: scope, + } +} + +func (a *SnapshotAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { + log.WithFields(log.Fields{ + "scope": scope, + "type": a.itemType, + }).Debug("SnapshotAdapter.List called") + + return cloneItems(a.index.GetItemsByTypeAndScope(a.itemType, scope)), nil +} + +// Search searches for items of this type by regex on GUN and includes 1-hop +// neighbors that also match this type and scope. +func (a *SnapshotAdapter) Search(ctx context.Context, scope string, query string, ignoreCache bool) ([]*sdp.Item, error) { + log.WithFields(log.Fields{ + "scope": scope, + "type": a.itemType, + "query": query, + }).Debug("SnapshotAdapter.Search called") + + regex, err := regexp.Compile(query) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: fmt.Sprintf("invalid regex pattern: %v", err), + Scope: scope, + } + } + + candidates := a.index.GetItemsByTypeAndScope(a.itemType, scope) + + var primaryMatches []*sdp.Item + for _, item := range candidates { + if regex.MatchString(item.GloballyUniqueName()) { + primaryMatches = append(primaryMatches, item) + } + } + + seen := make(map[string]bool, len(primaryMatches)) + for _, item := range primaryMatches { + seen[item.GloballyUniqueName()] = true + } + + var neighborMatches []*sdp.Item + for _, item := range primaryMatches { + for _, neighbor := range a.index.NeighborItems(item) { + if neighbor.GetType() != a.itemType { + continue + } + if scope != "*" && neighbor.GetScope() != scope { + continue + } + gun := neighbor.GloballyUniqueName() + if !seen[gun] { + seen[gun] = true + neighborMatches = append(neighborMatches, neighbor) + } + } + } + + result := make([]*sdp.Item, 0, len(primaryMatches)+len(neighborMatches)) + result = append(result, primaryMatches...) + result = append(result, neighborMatches...) + return cloneItems(result), nil +} + +func (a *SnapshotAdapter) Metadata() *sdp.AdapterMetadata { + return a.metadata +} diff --git a/sources/snapshot/adapters/adapter_test.go b/sources/snapshot/adapters/adapter_test.go new file mode 100644 index 00000000..7952ca2b --- /dev/null +++ b/sources/snapshot/adapters/adapter_test.go @@ -0,0 +1,297 @@ +package adapters + +import ( + "context" + "errors" + "testing" + + "github.com/overmindtech/cli/go/sdp-go" +) + +func createTestAdapters(t *testing.T) map[string]*SnapshotAdapter { + t.Helper() + snapshot := createTestSnapshot() + index, err := NewSnapshotIndex(snapshot) + if err != nil { + t.Fatalf("Failed to create test index: %v", err) + } + + adapters := make(map[string]*SnapshotAdapter) + for _, typ := range index.GetAllTypes() { + scopes := index.GetScopesForType(typ) + adapters[typ] = NewSnapshotAdapter(index, typ, scopes) + } + return adapters +} + +func TestAdapterType(t *testing.T) { + adapters := createTestAdapters(t) + + ec2 := adapters["ec2-instance"] + if ec2.Type() != "ec2-instance" { + t.Errorf("Expected type 'ec2-instance', got '%s'", ec2.Type()) + } + + s3 := adapters["s3-bucket"] + if s3.Type() != "s3-bucket" { + t.Errorf("Expected type 's3-bucket', got '%s'", s3.Type()) + } +} + +func TestAdapterName(t *testing.T) { + adapters := createTestAdapters(t) + + if adapters["ec2-instance"].Name() != "snapshot-ec2-instance" { + t.Errorf("Expected name 'snapshot-ec2-instance', got '%s'", adapters["ec2-instance"].Name()) + } +} + +func TestAdapterScopes(t *testing.T) { + adapters := createTestAdapters(t) + + ec2Scopes := adapters["ec2-instance"].Scopes() + if len(ec2Scopes) != 2 { + t.Fatalf("Expected 2 scopes for ec2-instance, got %d: %v", len(ec2Scopes), ec2Scopes) + } + scopeSet := map[string]bool{} + for _, s := range ec2Scopes { + scopeSet[s] = true + } + if !scopeSet["us-east-1"] || !scopeSet["us-west-2"] { + t.Errorf("Expected scopes [us-east-1, us-west-2], got %v", ec2Scopes) + } + + s3Scopes := adapters["s3-bucket"].Scopes() + if len(s3Scopes) != 1 || s3Scopes[0] != "global" { + t.Errorf("Expected scopes [global], got %v", s3Scopes) + } +} + +func TestAdapterGet(t *testing.T) { + adapters := createTestAdapters(t) + ec2 := adapters["ec2-instance"] + ctx := context.Background() + + // Get by unique attribute value with wildcard scope + item, err := ec2.Get(ctx, "*", "i-12345", false) + if err != nil { + t.Fatalf("Get failed: %v", err) + } + if item == nil || item.UniqueAttributeValue() != "i-12345" { + t.Errorf("Expected 'i-12345', got '%v'", item) + } + + // Get by GUN + item, err = ec2.Get(ctx, "*", "us-east-1.ec2-instance.i-12345", false) + if err != nil { + t.Fatalf("Get by GUN failed: %v", err) + } + if item == nil { + t.Fatal("Expected item by GUN, got nil") + } + + // Get with specific scope + item, err = ec2.Get(ctx, "us-east-1", "i-12345", false) + if err != nil { + t.Fatalf("Get with specific scope failed: %v", err) + } + if item == nil { + t.Fatal("Expected item, got nil") + } + + // Not found + _, err = ec2.Get(ctx, "*", "nonexistent", false) + if err == nil { + t.Error("Expected error for non-existent item") + } + var queryErr *sdp.QueryError + if !errors.As(err, &queryErr) || queryErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("Expected NOTFOUND, got %v", err) + } + + // Scope mismatch: requesting us-west-2 for an item in us-east-1 + _, err = ec2.Get(ctx, "us-west-2", "us-east-1.ec2-instance.i-12345", false) + if err == nil { + t.Fatal("Expected error when scope doesn't match GUN scope") + } + if !errors.As(err, &queryErr) || queryErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("Expected NOTFOUND, got %v", err) + } + + // Same GUN with matching scope works + item, err = ec2.Get(ctx, "us-east-1", "us-east-1.ec2-instance.i-12345", false) + if err != nil || item == nil || item.GetScope() != "us-east-1" { + t.Errorf("Get with matching scope should work: err=%v item=%v", err, item) + } + + // Cross-type: ec2 adapter should not return s3-bucket items + _, err = ec2.Get(ctx, "*", "my-test-bucket", false) + if err == nil { + t.Error("ec2 adapter should not find s3-bucket items") + } +} + +func TestAdapterList(t *testing.T) { + adapters := createTestAdapters(t) + ctx := context.Background() + + // ec2 adapter lists its 2 items + items, err := adapters["ec2-instance"].List(ctx, "*", false) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(items) != 2 { + t.Errorf("Expected 2 ec2-instance items, got %d", len(items)) + } + + // Verify linked items are preserved + var ec2East *sdp.Item + for _, item := range items { + if item.GloballyUniqueName() == "us-east-1.ec2-instance.i-12345" { + ec2East = item + break + } + } + if ec2East == nil { + t.Fatal("Expected to find ec2 instance i-12345") + } + linked := ec2East.GetLinkedItems() + if len(linked) != 1 { + t.Fatalf("Expected 1 linked item, got %d", len(linked)) + } + ref := linked[0].GetItem() + if ref.GetType() != "s3-bucket" || ref.GetUniqueAttributeValue() != "my-test-bucket" { + t.Errorf("Unexpected linked item reference: %v", ref) + } + + // List with specific scope + items, err = adapters["ec2-instance"].List(ctx, "us-east-1", false) + if err != nil { + t.Fatalf("List with specific scope failed: %v", err) + } + if len(items) != 1 { + t.Errorf("Expected 1 item for us-east-1, got %d", len(items)) + } + + // s3 adapter lists its 1 item + items, err = adapters["s3-bucket"].List(ctx, "*", false) + if err != nil { + t.Fatalf("s3 List failed: %v", err) + } + if len(items) != 1 { + t.Errorf("Expected 1 s3-bucket item, got %d", len(items)) + } + + // List with nonexistent scope + items, err = adapters["ec2-instance"].List(ctx, "nonexistent", false) + if err != nil { + t.Fatalf("List with nonexistent scope failed: %v", err) + } + if len(items) != 0 { + t.Errorf("Expected 0 items, got %d", len(items)) + } +} + +func TestAdapterSearch(t *testing.T) { + adapters := createTestAdapters(t) + ec2 := adapters["ec2-instance"] + ctx := context.Background() + + // Search matching both ec2 instances (neighbor s3-bucket is different type, not included) + items, err := ec2.Search(ctx, "*", ".*ec2-instance.*", false) + if err != nil { + t.Fatalf("Search failed: %v", err) + } + if len(items) != 2 { + t.Errorf("Expected 2 ec2-instance items, got %d", len(items)) + } + + // Search with specific scope + items, err = ec2.Search(ctx, "us-east-1", ".*ec2-instance.*", false) + if err != nil { + t.Fatalf("Search with specific scope failed: %v", err) + } + if len(items) != 1 { + t.Errorf("Expected 1 item in us-east-1, got %d", len(items)) + } + + // Search that matches nothing + items, err = ec2.Search(ctx, "*", "nonexistent-xyz", false) + if err != nil { + t.Fatalf("Search no match failed: %v", err) + } + if len(items) != 0 { + t.Errorf("Expected 0 items, got %d", len(items)) + } + + // Invalid regex + _, err = ec2.Search(ctx, "*", "[invalid(regex", false) + if err == nil { + t.Error("Expected error for invalid regex") + } + var queryErr *sdp.QueryError + if !errors.As(err, &queryErr) || queryErr.GetErrorType() != sdp.QueryError_OTHER { + t.Errorf("Expected OTHER error, got %v", err) + } +} + +func TestAdapterMetadata(t *testing.T) { + adapters := createTestAdapters(t) + + // ec2-instance should get metadata from the catalog + ec2Meta := adapters["ec2-instance"].Metadata() + if ec2Meta == nil { + t.Fatal("Expected metadata, got nil") + } + if ec2Meta.GetType() != "ec2-instance" { + t.Errorf("Expected type 'ec2-instance', got '%s'", ec2Meta.GetType()) + } + if ec2Meta.GetCategory() != sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION { + t.Errorf("Expected COMPUTE_APPLICATION category, got %v", ec2Meta.GetCategory()) + } + if ec2Meta.GetDescriptiveName() != "EC2 Instance" { + t.Errorf("Expected descriptive name 'EC2 Instance', got '%s'", ec2Meta.GetDescriptiveName()) + } + + methods := ec2Meta.GetSupportedQueryMethods() + if !methods.GetGet() || !methods.GetList() || !methods.GetSearch() { + t.Error("Expected all query methods to be supported for ec2-instance") + } + + // s3-bucket should also get catalog metadata + s3Meta := adapters["s3-bucket"].Metadata() + if s3Meta.GetType() != "s3-bucket" { + t.Errorf("Expected type 's3-bucket', got '%s'", s3Meta.GetType()) + } +} + +func TestNewSnapshotAdapter(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + adapter := NewSnapshotAdapter(index, "ec2-instance", []string{"us-east-1", "us-west-2"}) + if adapter == nil { + t.Fatal("Expected adapter, got nil") + } + if adapter.index != index { + t.Error("Expected adapter to store index reference") + } + if adapter.itemType != "ec2-instance" { + t.Errorf("Expected type 'ec2-instance', got '%s'", adapter.itemType) + } +} + +func TestAdapterMetadataFallback(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + // Use a type not in the catalog to test fallback + adapter := NewSnapshotAdapter(index, "unknown-type-xyz", []string{"test"}) + meta := adapter.Metadata() + if meta.GetType() != "unknown-type-xyz" { + t.Errorf("Expected type 'unknown-type-xyz', got '%s'", meta.GetType()) + } + if meta.GetCategory() != sdp.AdapterCategory_ADAPTER_CATEGORY_OTHER { + t.Errorf("Expected OTHER category for unknown type, got %v", meta.GetCategory()) + } +} diff --git a/sources/snapshot/adapters/catalog.go b/sources/snapshot/adapters/catalog.go new file mode 100644 index 00000000..e4e79f1d --- /dev/null +++ b/sources/snapshot/adapters/catalog.go @@ -0,0 +1,96 @@ +package adapters + +import ( + "encoding/json" + "io/fs" + + adapterdata "github.com/overmindtech/cli/docs.overmind.tech/docs/sources" + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" +) + +type catalogQueryMethods struct { + Get bool `json:"get"` + GetDescription string `json:"getDescription"` + List bool `json:"list"` + ListDescription string `json:"listDescription"` + Search bool `json:"search"` + SearchDescription string `json:"searchDescription"` +} + +type catalogEntry struct { + Type string `json:"type"` + Category int32 `json:"category"` + DescriptiveName string `json:"descriptiveName"` + PotentialLinks []string `json:"potentialLinks"` + SupportedQueryMethods catalogQueryMethods `json:"supportedQueryMethods"` +} + +var adapterCatalog map[string]*catalogEntry + +func init() { + adapterCatalog = make(map[string]*catalogEntry) + + err := fs.WalkDir(adapterdata.Files, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + + data, readErr := adapterdata.Files.ReadFile(path) + if readErr != nil { + log.WithError(readErr).WithField("path", path).Warn("Failed to read adapter data file") + return nil + } + + var entry catalogEntry + if jsonErr := json.Unmarshal(data, &entry); jsonErr != nil { + log.WithError(jsonErr).WithField("path", path).Warn("Failed to parse adapter data file") + return nil + } + + if entry.Type != "" { + adapterCatalog[entry.Type] = &entry + } + return nil + }) + if err != nil { + log.WithError(err).Error("Failed to walk embedded adapter data") + } +} + +// lookupAdapterMetadata returns AdapterMetadata for the given type by looking +// up the embedded catalog. Falls back to sensible defaults when the type is not +// in the catalog. +func lookupAdapterMetadata(itemType string, scopes []string) *sdp.AdapterMetadata { + entry, ok := adapterCatalog[itemType] + if !ok { + return &sdp.AdapterMetadata{ + Type: itemType, + DescriptiveName: itemType, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_OTHER, + } + } + + potentialLinks := make([]string, len(entry.PotentialLinks)) + copy(potentialLinks, entry.PotentialLinks) + + return &sdp.AdapterMetadata{ + Type: itemType, + DescriptiveName: entry.DescriptiveName, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: entry.SupportedQueryMethods.Get, + GetDescription: entry.SupportedQueryMethods.GetDescription, + List: entry.SupportedQueryMethods.List, + ListDescription: entry.SupportedQueryMethods.ListDescription, + Search: entry.SupportedQueryMethods.Search, + SearchDescription: entry.SupportedQueryMethods.SearchDescription, + }, + PotentialLinks: potentialLinks, + Category: sdp.AdapterCategory(entry.Category), + } +} diff --git a/sources/snapshot/adapters/index.go b/sources/snapshot/adapters/index.go new file mode 100644 index 00000000..fcbd92d0 --- /dev/null +++ b/sources/snapshot/adapters/index.go @@ -0,0 +1,241 @@ +package adapters + +import ( + "fmt" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" +) + +// SnapshotIndex maintains in-memory indices for efficient snapshot querying +type SnapshotIndex struct { + // All items in the snapshot + allItems []*sdp.Item + + // Index by GloballyUniqueName for fast GET lookups + byGUN map[string]*sdp.Item + + // Index by type and scope for filtering + byTypeScope map[string]map[string][]*sdp.Item + + // Edges from the snapshot (for future use) + edges []*sdp.Edge +} + +// NewSnapshotIndex builds indices from a snapshot +func NewSnapshotIndex(snapshot *sdp.Snapshot) (*SnapshotIndex, error) { + if snapshot == nil || snapshot.GetProperties() == nil { + return nil, fmt.Errorf("snapshot or properties is nil") + } + + items := snapshot.GetProperties().GetItems() + edges := snapshot.GetProperties().GetEdges() + + index := &SnapshotIndex{ + allItems: items, + byGUN: make(map[string]*sdp.Item), + byTypeScope: make(map[string]map[string][]*sdp.Item), + edges: edges, + } + + // Build indices + for _, item := range items { + gun := item.GloballyUniqueName() + index.byGUN[gun] = item + + itemType := item.GetType() + scope := item.GetScope() + + if index.byTypeScope[itemType] == nil { + index.byTypeScope[itemType] = make(map[string][]*sdp.Item) + } + index.byTypeScope[itemType][scope] = append(index.byTypeScope[itemType][scope], item) + } + + // Hydrate each item's LinkedItems from the snapshot edges so that + // callers (explore view, etc.) see the graph relationships directly on + // the returned items instead of having to cross-reference the separate + // edge list. + index.hydrateLinkedItems() + + log.WithFields(log.Fields{ + "total_items": len(items), + "total_edges": len(edges), + "types": len(index.byTypeScope), + }).Info("Snapshot index built") + + return index, nil +} + +// hydrateLinkedItems populates each item's LinkedItems field from the snapshot +// edges. For each edge, the item matching edge.From gets a LinkedItem pointing +// to edge.To (with blast propagation). Edges whose From item is not in the +// snapshot are skipped. +func (idx *SnapshotIndex) hydrateLinkedItems() { + // Build a map from item reference key → existing LinkedItem targets so + // we don't add duplicates when the item already carries some LinkedItems. + type refKey struct { + scope, typ, uav string + } + existingLinks := make(map[refKey]map[refKey]bool) + + for _, item := range idx.allItems { + key := refKey{item.GetScope(), item.GetType(), item.UniqueAttributeValue()} + set := make(map[refKey]bool) + for _, li := range item.GetLinkedItems() { + r := li.GetItem() + if r != nil { + set[refKey{r.GetScope(), r.GetType(), r.GetUniqueAttributeValue()}] = true + } + } + existingLinks[key] = set + } + + for _, edge := range idx.edges { + from := edge.GetFrom() + to := edge.GetTo() + if from == nil || to == nil { + continue + } + + item := idx.GetByReference(from) + if item == nil { + continue + } + + fromKey := refKey{item.GetScope(), item.GetType(), item.UniqueAttributeValue()} + toKey := refKey{to.GetScope(), to.GetType(), to.GetUniqueAttributeValue()} + + if existingLinks[fromKey][toKey] { + continue + } + + item.LinkedItems = append(item.LinkedItems, &sdp.LinkedItem{ + Item: to, + }) + existingLinks[fromKey][toKey] = true + } +} + +// GetAllItems returns all items in the snapshot +func (idx *SnapshotIndex) GetAllItems() []*sdp.Item { + return idx.allItems +} + +// GetByGUN retrieves an item by its GloballyUniqueName +func (idx *SnapshotIndex) GetByGUN(gun string) *sdp.Item { + return idx.byGUN[gun] +} + +// GetByReference retrieves an item by its Reference using the GUN index. +func (idx *SnapshotIndex) GetByReference(ref *sdp.Reference) *sdp.Item { + if ref == nil { + return nil + } + return idx.byGUN[ref.GloballyUniqueName()] +} + +// GetAllTypes returns all unique types in the snapshot +func (idx *SnapshotIndex) GetAllTypes() []string { + types := make([]string, 0, len(idx.byTypeScope)) + for itemType := range idx.byTypeScope { + types = append(types, itemType) + } + return types +} + +// GetScopesForType returns all unique scopes that contain items of the given type. +func (idx *SnapshotIndex) GetScopesForType(itemType string) []string { + scopeMap, ok := idx.byTypeScope[itemType] + if !ok { + return nil + } + scopes := make([]string, 0, len(scopeMap)) + for s := range scopeMap { + scopes = append(scopes, s) + } + return scopes +} + +// GetItemsByTypeAndScope returns items matching the given type and scope. +// A wildcard ("*") scope returns all items of that type. +func (idx *SnapshotIndex) GetItemsByTypeAndScope(itemType, scope string) []*sdp.Item { + scopeMap, ok := idx.byTypeScope[itemType] + if !ok { + return nil + } + if scope == "*" { + var all []*sdp.Item + for _, items := range scopeMap { + all = append(all, items...) + } + return all + } + return scopeMap[scope] +} + +// EdgesFrom returns all edges whose From reference equals ref. +func (idx *SnapshotIndex) EdgesFrom(ref *sdp.Reference) []*sdp.Edge { + if ref == nil { + return nil + } + var out []*sdp.Edge + for _, e := range idx.edges { + if e.GetFrom() != nil && e.GetFrom().IsEqual(ref) { + out = append(out, e) + } + } + return out +} + +// EdgesTo returns all edges whose To reference equals ref. +func (idx *SnapshotIndex) EdgesTo(ref *sdp.Reference) []*sdp.Edge { + if ref == nil { + return nil + } + var out []*sdp.Edge + for _, e := range idx.edges { + if e.GetTo() != nil && e.GetTo().IsEqual(ref) { + out = append(out, e) + } + } + return out +} + +// NeighborItems returns items that are connected to the given item by any edge +// (as From or To). Each item is returned at most once. Items not present in +// the snapshot are skipped. +func (idx *SnapshotIndex) NeighborItems(item *sdp.Item) []*sdp.Item { + if item == nil { + return nil + } + ref := item.Reference() + seen := make(map[string]bool) + var out []*sdp.Item + for _, e := range idx.EdgesFrom(ref) { + if e.GetTo() != nil { + other := idx.GetByReference(e.GetTo()) + if other != nil { + gun := other.GloballyUniqueName() + if !seen[gun] { + seen[gun] = true + out = append(out, other) + } + } + } + } + for _, e := range idx.EdgesTo(ref) { + if e.GetFrom() != nil { + other := idx.GetByReference(e.GetFrom()) + if other != nil { + gun := other.GloballyUniqueName() + if !seen[gun] { + seen[gun] = true + out = append(out, other) + } + } + } + } + return out +} + diff --git a/sources/snapshot/adapters/index_test.go b/sources/snapshot/adapters/index_test.go new file mode 100644 index 00000000..0cb4a6ce --- /dev/null +++ b/sources/snapshot/adapters/index_test.go @@ -0,0 +1,288 @@ +package adapters + +import ( + "testing" + + "github.com/overmindtech/cli/go/sdp-go" +) + +func createTestSnapshot() *sdp.Snapshot { + attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "instanceId": "i-12345", + "name": "test-instance", + }) + attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "instanceId": "i-67890", + "name": "test-instance-2", + }) + attrs3, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "bucketName": "my-test-bucket", + }) + + return &sdp.Snapshot{ + Properties: &sdp.SnapshotProperties{ + Name: "test-snapshot", + Items: []*sdp.Item{ + { + Type: "ec2-instance", + UniqueAttribute: "instanceId", + Attributes: attrs1, + Scope: "us-east-1", + }, + { + Type: "ec2-instance", + UniqueAttribute: "instanceId", + Attributes: attrs2, + Scope: "us-west-2", + }, + { + Type: "s3-bucket", + UniqueAttribute: "bucketName", + Attributes: attrs3, + Scope: "global", + }, + }, + Edges: []*sdp.Edge{ + { + From: &sdp.Reference{ + Type: "ec2-instance", + UniqueAttributeValue: "i-12345", + Scope: "us-east-1", + }, + To: &sdp.Reference{ + Type: "s3-bucket", + UniqueAttributeValue: "my-test-bucket", + Scope: "global", + }, + }, + }, + }, + } +} + +func TestNewSnapshotIndex(t *testing.T) { + snapshot := createTestSnapshot() + + index, err := NewSnapshotIndex(snapshot) + if err != nil { + t.Fatalf("NewSnapshotIndex failed: %v", err) + } + + if index == nil { + t.Fatal("Expected index to be non-nil") + } + + // Verify all items are indexed + allItems := index.GetAllItems() + if len(allItems) != 3 { + t.Errorf("Expected 3 items, got %d", len(allItems)) + } + + // Verify edges are stored + if len(index.edges) != 1 { + t.Errorf("Expected 1 edge, got %d", len(index.edges)) + } +} + +func TestLinkedItemsHydrated(t *testing.T) { + snapshot := createTestSnapshot() + index, err := NewSnapshotIndex(snapshot) + if err != nil { + t.Fatalf("NewSnapshotIndex failed: %v", err) + } + + // The ec2-instance i-12345 is the From side of the edge to s3-bucket + ec2 := index.GetByGUN("us-east-1.ec2-instance.i-12345") + if ec2 == nil { + t.Fatal("expected to find ec2 instance") + } + linked := ec2.GetLinkedItems() + if len(linked) != 1 { + t.Fatalf("Expected 1 linked item on ec2 instance, got %d", len(linked)) + } + ref := linked[0].GetItem() + if ref.GetType() != "s3-bucket" || ref.GetUniqueAttributeValue() != "my-test-bucket" || ref.GetScope() != "global" { + t.Errorf("Unexpected linked item reference: %v", ref) + } + + // The s3-bucket is only on the To side of the edge, so it should have no LinkedItems + bucket := index.GetByGUN("global.s3-bucket.my-test-bucket") + if bucket == nil { + t.Fatal("expected to find s3 bucket") + } + if len(bucket.GetLinkedItems()) != 0 { + t.Errorf("Expected 0 linked items on bucket (it is only a To target), got %d", len(bucket.GetLinkedItems())) + } + + // The us-west-2 instance has no edges at all + ec2West := index.GetByGUN("us-west-2.ec2-instance.i-67890") + if ec2West == nil { + t.Fatal("expected to find us-west-2 ec2 instance") + } + if len(ec2West.GetLinkedItems()) != 0 { + t.Errorf("Expected 0 linked items on us-west-2 instance, got %d", len(ec2West.GetLinkedItems())) + } +} + +func TestGetByGUN(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + // Test getting item by GUN + gun := "us-east-1.ec2-instance.i-12345" + item := index.GetByGUN(gun) + if item == nil { + t.Fatalf("Expected to find item with GUN %s", gun) + } + + if item.UniqueAttributeValue() != "i-12345" { + t.Errorf("Expected unique attribute 'i-12345', got '%s'", item.UniqueAttributeValue()) + } + + // Test non-existent GUN + item = index.GetByGUN("nonexistent.type.query") + if item != nil { + t.Error("Expected nil for non-existent GUN") + } +} + +func TestGetByReference(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + // Test getting item by reference + ref := &sdp.Reference{ + Type: "ec2-instance", + UniqueAttributeValue: "i-12345", + Scope: "us-east-1", + } + + item := index.GetByReference(ref) + if item == nil { + t.Fatal("Expected to find item by reference") + } + + if item.UniqueAttributeValue() != "i-12345" { + t.Errorf("Expected unique attribute 'i-12345', got '%s'", item.UniqueAttributeValue()) + } +} + +func TestGetAllTypes(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + types := index.GetAllTypes() + if len(types) != 2 { + t.Errorf("Expected 2 unique types, got %d", len(types)) + } + + // Verify expected types exist + typeMap := make(map[string]bool) + for _, itemType := range types { + typeMap[itemType] = true + } + + expectedTypes := []string{"ec2-instance", "s3-bucket"} + for _, expected := range expectedTypes { + if !typeMap[expected] { + t.Errorf("Expected type '%s' not found", expected) + } + } +} + +func TestEdgesFromAndEdgesTo(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + refFrom := &sdp.Reference{ + Type: "ec2-instance", + UniqueAttributeValue: "i-12345", + Scope: "us-east-1", + } + refTo := &sdp.Reference{ + Type: "s3-bucket", + UniqueAttributeValue: "my-test-bucket", + Scope: "global", + } + + fromEdges := index.EdgesFrom(refFrom) + if len(fromEdges) != 1 { + t.Errorf("Expected 1 edge from ec2-instance i-12345, got %d", len(fromEdges)) + } + if len(fromEdges) > 0 && !fromEdges[0].GetTo().IsEqual(refTo) { + t.Error("EdgesFrom: expected To reference to be s3-bucket my-test-bucket") + } + + toEdges := index.EdgesTo(refTo) + if len(toEdges) != 1 { + t.Errorf("Expected 1 edge to s3-bucket my-test-bucket, got %d", len(toEdges)) + } + if len(toEdges) > 0 && !toEdges[0].GetFrom().IsEqual(refFrom) { + t.Error("EdgesTo: expected From reference to be ec2-instance i-12345") + } + + // No edges from the bucket (it only appears as To) + fromBucket := index.EdgesFrom(refTo) + if len(fromBucket) != 0 { + t.Errorf("Expected 0 edges from bucket, got %d", len(fromBucket)) + } + // No edges to the us-east-1 instance (it only appears as From in this snapshot) + toInstance := index.EdgesTo(refFrom) + if len(toInstance) != 0 { + t.Errorf("Expected 0 edges to us-east-1 instance, got %d", len(toInstance)) + } +} + +func TestNeighborItems(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + ec2East := index.GetByGUN("us-east-1.ec2-instance.i-12345") + if ec2East == nil { + t.Fatal("expected to find us-east-1 ec2 instance") + } + neighbors := index.NeighborItems(ec2East) + if len(neighbors) != 1 { + t.Fatalf("Expected 1 neighbor of us-east-1 ec2 instance, got %d", len(neighbors)) + } + if neighbors[0].GloballyUniqueName() != "global.s3-bucket.my-test-bucket" { + t.Errorf("Expected neighbor to be s3-bucket, got %s", neighbors[0].GloballyUniqueName()) + } + + bucket := index.GetByGUN("global.s3-bucket.my-test-bucket") + if bucket == nil { + t.Fatal("expected to find s3 bucket") + } + neighbors = index.NeighborItems(bucket) + if len(neighbors) != 1 { + t.Fatalf("Expected 1 neighbor of s3 bucket, got %d", len(neighbors)) + } + if neighbors[0].GloballyUniqueName() != "us-east-1.ec2-instance.i-12345" { + t.Errorf("Expected neighbor to be ec2-instance i-12345, got %s", neighbors[0].GloballyUniqueName()) + } + + // us-west-2 instance has no edges + ec2West := index.GetByGUN("us-west-2.ec2-instance.i-67890") + if ec2West == nil { + t.Fatal("expected to find us-west-2 ec2 instance") + } + neighbors = index.NeighborItems(ec2West) + if len(neighbors) != 0 { + t.Errorf("Expected 0 neighbors for us-west-2 instance, got %d", len(neighbors)) + } +} + +func TestNewSnapshotIndexNilSnapshot(t *testing.T) { + _, err := NewSnapshotIndex(nil) + if err == nil { + t.Error("Expected error for nil snapshot, got nil") + } +} + +func TestNewSnapshotIndexNilProperties(t *testing.T) { + snapshot := &sdp.Snapshot{} + _, err := NewSnapshotIndex(snapshot) + if err == nil { + t.Error("Expected error for nil properties, got nil") + } +} diff --git a/sources/snapshot/adapters/loader.go b/sources/snapshot/adapters/loader.go new file mode 100644 index 00000000..d0dcb96a --- /dev/null +++ b/sources/snapshot/adapters/loader.go @@ -0,0 +1,89 @@ +package adapters + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" +) + +// LoadSnapshot loads a snapshot from a URL or local file path +func LoadSnapshot(ctx context.Context, source string) (*sdp.Snapshot, error) { + var data []byte + var err error + + // Determine if source is a URL or file path + if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { + log.WithField("url", source).Info("Loading snapshot from URL") + data, err = loadSnapshotFromURL(ctx, source) + if err != nil { + return nil, fmt.Errorf("failed to load snapshot from URL: %w", err) + } + } else { + log.WithField("path", source).Info("Loading snapshot from file") + data, err = loadSnapshotFromFile(source) + if err != nil { + return nil, fmt.Errorf("failed to load snapshot from file: %w", err) + } + } + + // Unmarshal the protobuf data + snapshot := &sdp.Snapshot{} + if err := proto.Unmarshal(data, snapshot); err != nil { + return nil, fmt.Errorf("failed to unmarshal snapshot protobuf: %w", err) + } + + // Validate snapshot has items + if snapshot.GetProperties() == nil || len(snapshot.GetProperties().GetItems()) == 0 { + return nil, fmt.Errorf("snapshot has no items") + } + + log.WithFields(log.Fields{ + "items": len(snapshot.GetProperties().GetItems()), + "edges": len(snapshot.GetProperties().GetEdges()), + }).Info("Snapshot loaded successfully") + + return snapshot, nil +} + +// loadSnapshotFromURL loads snapshot data from an HTTP(S) URL +func loadSnapshotFromURL(ctx context.Context, url string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP request returned status %d", resp.StatusCode) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + return data, nil +} + +// loadSnapshotFromFile loads snapshot data from a local file +func loadSnapshotFromFile(path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + return data, nil +} diff --git a/sources/snapshot/adapters/loader_test.go b/sources/snapshot/adapters/loader_test.go new file mode 100644 index 00000000..7ae5d63b --- /dev/null +++ b/sources/snapshot/adapters/loader_test.go @@ -0,0 +1,161 @@ +package adapters + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/overmindtech/cli/go/sdp-go" + "google.golang.org/protobuf/proto" +) + +func TestLoadSnapshotFromFile(t *testing.T) { + // Create a test snapshot + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "test-item", + }) + + snapshot := &sdp.Snapshot{ + Properties: &sdp.SnapshotProperties{ + Name: "test-snapshot", + Items: []*sdp.Item{ + { + Type: "test-type", + UniqueAttribute: "name", + Attributes: attrs, + Scope: "test-scope", + }, + }, + }, + } + + // Marshal to bytes + data, err := proto.Marshal(snapshot) + if err != nil { + t.Fatalf("Failed to marshal test snapshot: %v", err) + } + + // Write to temp file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "test-snapshot.pb") + if err := os.WriteFile(tmpFile, data, 0o644); err != nil { + t.Fatalf("Failed to write test snapshot file: %v", err) + } + + // Test loading + ctx := context.Background() + loaded, err := LoadSnapshot(ctx, tmpFile) + if err != nil { + t.Fatalf("LoadSnapshot failed: %v", err) + } + + if loaded.GetProperties().GetName() != "test-snapshot" { + t.Errorf("Expected snapshot name 'test-snapshot', got '%s'", loaded.GetProperties().GetName()) + } + + if len(loaded.GetProperties().GetItems()) != 1 { + t.Errorf("Expected 1 item, got %d", len(loaded.GetProperties().GetItems())) + } +} + +func TestLoadSnapshotFromURL(t *testing.T) { + // Create a test snapshot + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "test-item", + }) + + snapshot := &sdp.Snapshot{ + Properties: &sdp.SnapshotProperties{ + Name: "test-snapshot-url", + Items: []*sdp.Item{ + { + Type: "test-type", + UniqueAttribute: "name", + Attributes: attrs, + Scope: "test-scope", + }, + }, + }, + } + + // Marshal to bytes + data, err := proto.Marshal(snapshot) + if err != nil { + t.Fatalf("Failed to marshal test snapshot: %v", err) + } + + // Create test HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(data) + })) + defer server.Close() + + // Test loading from URL + ctx := context.Background() + loaded, err := LoadSnapshot(ctx, server.URL) + if err != nil { + t.Fatalf("LoadSnapshot from URL failed: %v", err) + } + + if loaded.GetProperties().GetName() != "test-snapshot-url" { + t.Errorf("Expected snapshot name 'test-snapshot-url', got '%s'", loaded.GetProperties().GetName()) + } +} + +func TestLoadSnapshotEmptyItems(t *testing.T) { + // Create a snapshot with no items + snapshot := &sdp.Snapshot{ + Properties: &sdp.SnapshotProperties{ + Name: "empty-snapshot", + Items: []*sdp.Item{}, + }, + } + + // Marshal to bytes + data, err := proto.Marshal(snapshot) + if err != nil { + t.Fatalf("Failed to marshal test snapshot: %v", err) + } + + // Write to temp file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "empty-snapshot.pb") + if err := os.WriteFile(tmpFile, data, 0o644); err != nil { + t.Fatalf("Failed to write test snapshot file: %v", err) + } + + // Test loading - should fail validation + ctx := context.Background() + _, err = LoadSnapshot(ctx, tmpFile) + if err == nil { + t.Error("Expected error for snapshot with no items, got nil") + } +} + +func TestLoadSnapshotFileNotFound(t *testing.T) { + ctx := context.Background() + _, err := LoadSnapshot(ctx, "/nonexistent/file.pb") + if err == nil { + t.Error("Expected error for nonexistent file, got nil") + } +} + +func TestLoadSnapshotInvalidProtobuf(t *testing.T) { + // Write invalid protobuf data + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "invalid.pb") + if err := os.WriteFile(tmpFile, []byte("invalid protobuf data"), 0o644); err != nil { + t.Fatalf("Failed to write invalid data: %v", err) + } + + // Test loading - should fail + ctx := context.Background() + _, err := LoadSnapshot(ctx, tmpFile) + if err == nil { + t.Error("Expected error for invalid protobuf, got nil") + } +} diff --git a/sources/snapshot/adapters/main.go b/sources/snapshot/adapters/main.go new file mode 100644 index 00000000..91f570fc --- /dev/null +++ b/sources/snapshot/adapters/main.go @@ -0,0 +1,45 @@ +package adapters + +import ( + "context" + "fmt" + + "github.com/overmindtech/cli/go/discovery" + log "github.com/sirupsen/logrus" +) + +// InitializeAdapters loads a snapshot and registers one adapter per type found +// in the snapshot data. Each adapter carries the correct category and metadata +// from the embedded adapter catalog so that the discovery engine can route +// specific-type GET/SEARCH queries to it. +func InitializeAdapters(ctx context.Context, e *discovery.Engine, snapshotSource string) error { + snapshot, err := LoadSnapshot(ctx, snapshotSource) + if err != nil { + return fmt.Errorf("failed to load snapshot: %w", err) + } + + index, err := NewSnapshotIndex(snapshot) + if err != nil { + return fmt.Errorf("failed to build snapshot index: %w", err) + } + + types := index.GetAllTypes() + adapters := make([]discovery.Adapter, 0, len(types)) + for _, typ := range types { + scopes := index.GetScopesForType(typ) + adapters = append(adapters, NewSnapshotAdapter(index, typ, scopes)) + } + + if err := e.AddAdapters(adapters...); err != nil { + return fmt.Errorf("failed to add snapshot adapters: %w", err) + } + + log.WithFields(log.Fields{ + "items": len(snapshot.GetProperties().GetItems()), + "edges": len(snapshot.GetProperties().GetEdges()), + "types": len(types), + "adapters": len(adapters), + }).Info("Snapshot adapters initialized successfully") + + return nil +} diff --git a/sources/snapshot/cmd/root.go b/sources/snapshot/cmd/root.go new file mode 100644 index 00000000..0882ba22 --- /dev/null +++ b/sources/snapshot/cmd/root.go @@ -0,0 +1,233 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/getsentry/sentry-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/logging" + "github.com/overmindtech/cli/go/tracing" + "github.com/overmindtech/cli/sources/snapshot/adapters" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var cfgFile string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "snapshot-source", + Short: "Discovery source that serves data from a snapshot file", + SilenceUsage: true, + Long: `Snapshot source loads a snapshot from a file or URL and responds to +discovery queries with items from that snapshot. This enables local testing +with fixed data and deterministic re-runs of v6 investigations.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + defer tracing.LogRecoverToReturn(ctx, "snapshot-source.root") + + // Get snapshot source (required) + snapshotSource := viper.GetString("snapshot-source") + if snapshotSource == "" { + return fmt.Errorf("snapshot-source is required (use --snapshot-source or SNAPSHOT_SOURCE env var)") + } + + log.WithField("snapshot-source", snapshotSource).Info("Starting snapshot source") + + // Get engine config + engineConfig, err := discovery.EngineConfigFromViper("snapshot", tracing.Version()) + if err != nil { + log.WithError(err).Error("Could not get engine config from viper") + return fmt.Errorf("could not get engine config from viper: %w", err) + } + + // Create a basic engine first + e, err := discovery.NewEngine(engineConfig) + if err != nil { + sentry.CaptureException(err) + log.WithError(err).Error("Could not create engine") + return fmt.Errorf("could not create engine: %w", err) + } + + // Start HTTP server for health checks before initialization + healthCheckPort := viper.GetInt("health-check-port") + e.ServeHealthProbes(healthCheckPort) + + // Start the engine (NATS connection) before adapter init so heartbeats work + err = e.Start(ctx) + if err != nil { + sentry.CaptureException(err) + log.WithError(err).Error("Could not start engine") + return fmt.Errorf("could not start engine: %w", err) + } + + // Snapshot adapters load from files/URLs which may fail, so we use + // the initialization pattern with error handling + err = adapters.InitializeAdapters(ctx, e, snapshotSource) + if err != nil { + initErr := fmt.Errorf("could not initialize snapshot adapters: %w", err) + log.WithError(initErr).Error("Snapshot source initialization failed - pod will stay running with error status") + e.SetInitError(initErr) + sentry.CaptureException(initErr) + } else { + e.StartSendingHeartbeats(ctx) + } + + <-ctx.Done() + + log.Info("Stopping engine") + + err = e.Stop() + if err != nil { + log.WithError(err).Error("Could not stop engine") + return fmt.Errorf("could not stop engine: %w", err) + } + + log.Info("Stopped") + + return nil + }, +} + +// Execute adds all child commands to the root command and sets flags +// appropriately. This is called by main.main(). It only needs to happen once to +// the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + var logLevel string + + // General config options + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "/etc/srcman/config/source.yaml", "config file path") + rootCmd.PersistentFlags().StringVar(&logLevel, "log", "info", "Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace") + cobra.CheckErr(viper.BindEnv("log", "SNAPSHOT_LOG", "LOG")) // fallback to global config + + // Snapshot-specific config + rootCmd.PersistentFlags().String("snapshot-source", "", "Path to snapshot file or URL to load (required). Can be a local file path or http(s) URL.") + cobra.CheckErr(viper.BindEnv("snapshot-source", "SNAPSHOT_SOURCE", "SNAPSHOT_PATH", "SNAPSHOT_URL")) + + // engine config options + discovery.AddEngineFlags(rootCmd) + + rootCmd.PersistentFlags().IntP("health-check-port", "", 8089, "The port that the health check should run on") + cobra.CheckErr(viper.BindEnv("health-check-port", "SNAPSHOT_HEALTH_CHECK_PORT", "HEALTH_CHECK_PORT", "SNAPSHOT_SERVICE_PORT", "SERVICE_PORT")) // new names + backwards compat + + // tracing + rootCmd.PersistentFlags().String("honeycomb-api-key", "", "If specified, configures opentelemetry libraries to submit traces to honeycomb") + cobra.CheckErr(viper.BindEnv("honeycomb-api-key", "SNAPSHOT_HONEYCOMB_API_KEY", "HONEYCOMB_API_KEY")) // fallback to global config + rootCmd.PersistentFlags().String("sentry-dsn", "", "If specified, configures sentry libraries to capture errors") + cobra.CheckErr(viper.BindEnv("sentry-dsn", "SNAPSHOT_SENTRY_DSN", "SENTRY_DSN")) // fallback to global config + rootCmd.PersistentFlags().String("run-mode", "release", "Set the run mode for this service, 'release', 'debug' or 'test'. Defaults to 'release'.") + rootCmd.PersistentFlags().Bool("json-log", true, "Set to false to emit logs as text for easier reading in development.") + cobra.CheckErr(viper.BindEnv("json-log", "SNAPSHOT_SOURCE_JSON_LOG", "JSON_LOG")) // fallback to global config + + // Bind these to viper + cobra.CheckErr(viper.BindPFlags(rootCmd.PersistentFlags())) + + // Run this before we do anything to set up the loglevel + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if lvl, err := log.ParseLevel(logLevel); err == nil { + log.SetLevel(lvl) + } else { + log.SetLevel(log.InfoLevel) + log.WithFields(log.Fields{ + "error": err, + }).Error("Could not parse log level") + } + + log.AddHook(TerminationLogHook{}) + + // Bind flags that haven't been set to the values from viper of we have them + var bindErr error + cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { + // Bind the flag to viper only if it has a non-empty default + if f.DefValue != "" || f.Changed { + if err := viper.BindPFlag(f.Name, f); err != nil { + bindErr = err + } + } + }) + if bindErr != nil { + log.WithError(bindErr).Error("Could not bind flag to viper") + return fmt.Errorf("could not bind flag to viper: %w", bindErr) + } + + if viper.GetBool("json-log") { + logging.ConfigureLogrusJSON(log.StandardLogger()) + } + + if err := tracing.InitTracerWithUpstreams("snapshot-source", viper.GetString("honeycomb-api-key"), viper.GetString("sentry-dsn")); err != nil { + log.WithError(err).Error("could not init tracer") + return fmt.Errorf("could not init tracer: %w", err) + } + return nil + } + + // shut down tracing at the end of the process + rootCmd.PersistentPostRun = func(cmd *cobra.Command, args []string) { + tracing.ShutdownTracer(context.Background()) + } +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + viper.SetConfigFile(cfgFile) + + replacer := strings.NewReplacer("-", "_") + + viper.SetEnvKeyReplacer(replacer) + // Do not set env prefix so APP, API_KEY, NATS_* etc. are read the same as other sources (aws, gcp). + // Snapshot-specific options use explicit BindEnv (e.g. SNAPSHOT_SOURCE) in flag init. + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + log.Infof("Using config file: %v", viper.ConfigFileUsed()) + } +} + +// TerminationLogHook A hook that logs fatal errors to the termination log +type TerminationLogHook struct{} + +func (t TerminationLogHook) Levels() []log.Level { + return []log.Level{log.FatalLevel} +} + +func (t TerminationLogHook) Fire(e *log.Entry) error { + // shutdown tracing first to ensure all spans are flushed + tracing.ShutdownTracer(context.Background()) + tLog, err := os.OpenFile("/dev/termination-log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + + var message string + + message = e.Message + + for k, v := range e.Data { + message = fmt.Sprintf("%v %v=%v", message, k, v) + } + + _, err = tLog.WriteString(message) + + return err +} diff --git a/sources/snapshot/main.go b/sources/snapshot/main.go new file mode 100644 index 00000000..c5754cd1 --- /dev/null +++ b/sources/snapshot/main.go @@ -0,0 +1,11 @@ +package main + +import ( + _ "go.uber.org/automaxprocs" + + "github.com/overmindtech/cli/sources/snapshot/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/sources/transformer.go b/sources/transformer.go index 4d673366..92c9f0d0 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -2,15 +2,16 @@ package sources import ( "context" + "errors" "fmt" "strings" "buf.build/go/protovalidate" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" azureshared "github.com/overmindtech/cli/sources/azure/shared" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -252,6 +253,20 @@ func (s *standardAdapterCore) validateScopes(scope string) error { } } +// NOTFOUND caching contract (applies to all adapters using this transformer, including manual adapters): +// we only cache when the result is "not found" (not timeouts or other errors). When a second call hits +// the cache, we return the same response and error as a fresh not-found call (e.g. Get: nil item + same +// error message; List/Search: empty slice + nil error). No behavior change. +// +// IsNotFound returns true if err is a QueryError with ErrorType NOTFOUND. +func IsNotFound(err error) bool { + var qe *sdp.QueryError + if errors.As(err, &qe) { + return qe.GetErrorType() == sdp.QueryError_NOTFOUND + } + return false +} + // Get retrieves a single item with a given scope and query. func (s *standardAdapterCore) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { if err := s.validateScopes(scope); err != nil { @@ -270,13 +285,18 @@ func (s *standardAdapterCore) Get(ctx context.Context, scope string, query strin defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into nil result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return nil, qErr + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": s.sourceType, "ovm.source.adapter": s.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_GET.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit && len(cachedItem) > 0 { @@ -294,10 +314,27 @@ func (s *standardAdapterCore) Get(ctx context.Context, scope string, query strin item, err := s.wrapper.Get(ctx, scope, queryParts...) if err != nil { - s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + // Only cache NOTFOUND so lookup behaviour is unchanged for timeouts/other errors + if IsNotFound(err) { + s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } + if item == nil { + // Cache not-found when item is nil + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("%s not found for query '%s'", s.Type(), query), + Scope: scope, + SourceName: s.Name(), + ItemType: s.Type(), + ResponderName: s.Name(), + } + s.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return nil, notFoundErr + } + // Store in cache after successful get s.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) return item, nil @@ -381,13 +418,18 @@ func (s *standardListableAdapterImpl) List(ctx context.Context, scope string, ig defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": s.sourceType, "ovm.source.adapter": s.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_LIST.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit { @@ -396,10 +438,27 @@ func (s *standardListableAdapterImpl) List(ctx context.Context, scope string, ig items, err := s.listable.List(ctx, scope) if err != nil { - s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + // Only cache NOTFOUND so lookup behaviour is unchanged for timeouts/other errors + if IsNotFound(err) { + s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found in scope %s", s.Type(), scope), + Scope: scope, + SourceName: s.Name(), + ItemType: s.Type(), + ResponderName: s.Name(), + } + s.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + for _, item := range items { s.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) } @@ -430,13 +489,19 @@ func (s *standardListableAdapterImpl) ListStream(ctx context.Context, scope stri defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": s.sourceType, "ovm.source.adapter": s.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_LIST.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { @@ -613,11 +678,64 @@ func (s *standardSearchableAdapterImpl) Search(ctx context.Context, scope string ) } + // Check cache before searching + cacheHit, ck, cachedItems, qErr, done := s.cache.Lookup( + ctx, + s.Name(), + sdp.QueryMethod_SEARCH, + scope, + s.Type(), + query, + ignoreCache, + ) + defer done() + + if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } + log.WithContext(ctx).WithFields(log.Fields{ + "ovm.source.type": s.sourceType, + "ovm.source.adapter": s.Name(), + "ovm.source.scope": scope, + "ovm.source.method": sdp.QueryMethod_SEARCH.String(), + "ovm.source.cache-key": ck, + }).WithError(qErr).Info("returning cached query error") + return nil, qErr + } + + if cacheHit { + return cachedItems, nil + } + items, err := s.searchable.Search(ctx, scope, queryParts...) if err != nil { + // Only cache NOTFOUND so lookup behaviour is unchanged for timeouts/other errors + if IsNotFound(err) { + s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found for search query '%s'", s.Type(), query), + Scope: scope, + SourceName: s.Name(), + ItemType: s.Type(), + ResponderName: s.Name(), + } + s.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + + for _, item := range items { + s.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) + } + return items, nil } @@ -639,13 +757,19 @@ func (s *standardSearchableAdapterImpl) SearchStream(ctx context.Context, scope defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": s.sourceType, "ovm.source.adapter": s.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { diff --git a/sources/transformer_test.go b/sources/transformer_test.go index acbed0c0..ac18c762 100644 --- a/sources/transformer_test.go +++ b/sources/transformer_test.go @@ -2,13 +2,14 @@ package sources import ( "context" + "errors" "sync" "sync/atomic" "testing" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" aws "github.com/overmindtech/cli/sources/aws/shared" gcp "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" @@ -206,10 +207,12 @@ func TestListErrorCausesCacheHang(t *testing.T) { t.Logf(" List() called %d times", mockWrapper.callCount.Load()) } - // List() is called twice - once by first, once by second after being woken + // We only cache NOTFOUND; this wrapper returns QueryError_OTHER so the error is not cached. + // Both goroutines call List() (callCount == 2). The important assertion is timing above: + // second goroutine completes quickly because done() wakes it, then it retries and gets the same error. callCount := mockWrapper.callCount.Load() if callCount != 2 { - t.Errorf("Expected List to be called twice, was called %d times", callCount) + t.Errorf("Expected List to be called twice (error is not cached), was called %d times", callCount) } t.Logf("Test results:") @@ -217,3 +220,293 @@ func TestListErrorCausesCacheHang(t *testing.T) { t.Logf(" Second goroutine: %v", secondDuration) t.Logf(" List() calls: %d", callCount) } + +// notFoundCachingWrapper returns nil/empty from Get/List/Search to test NOTFOUND caching. +type notFoundCachingWrapper struct { + getCallCount atomic.Int32 + listCallCount atomic.Int32 + searchCallCount atomic.Int32 + itemType shared.ItemType + scope string +} + +func (w *notFoundCachingWrapper) Scopes() []string { + return []string{w.scope} +} + +func (w *notFoundCachingWrapper) GetLookups() ItemTypeLookups { + return ItemTypeLookups{shared.NewItemTypeLookup("id", w.itemType)} +} + +func (w *notFoundCachingWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + w.getCallCount.Add(1) + return nil, nil +} + +func (w *notFoundCachingWrapper) Type() string { + return w.itemType.String() +} + +func (w *notFoundCachingWrapper) Name() string { + return "notfound-caching-adapter" +} + +func (w *notFoundCachingWrapper) ItemType() shared.ItemType { + return w.itemType +} + +func (w *notFoundCachingWrapper) TerraformMappings() []*sdp.TerraformMapping { + return nil +} + +func (w *notFoundCachingWrapper) Category() sdp.AdapterCategory { + return sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION +} + +func (w *notFoundCachingWrapper) PotentialLinks() map[shared.ItemType]bool { + return nil +} + +func (w *notFoundCachingWrapper) AdapterMetadata() *sdp.AdapterMetadata { + return nil +} + +func (w *notFoundCachingWrapper) IAMPermissions() []string { + return nil +} + +func (w *notFoundCachingWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + w.listCallCount.Add(1) + return []*sdp.Item{}, nil +} + +func (w *notFoundCachingWrapper) SearchLookups() []ItemTypeLookups { + return []ItemTypeLookups{{shared.NewItemTypeLookup("id", w.itemType)}} +} + +func (w *notFoundCachingWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + w.searchCallCount.Add(1) + return []*sdp.Item{}, nil +} + +// TestGetNilCachesNotFound tests that when wrapper Get returns (nil, nil), the adapter +// caches NOTFOUND and a second Get returns the cached error without calling the wrapper again. +func TestGetNilCachesNotFound(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + // Use AWS item type so adapter validation does not require GCP predefined role. + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache) + + // First Get: miss, wrapper returns (nil, nil), adapter caches NOTFOUND + item, err := adapter.Get(ctx, scope, "query1", false) + if item != nil { + t.Errorf("first Get: expected nil item, got %v", item) + } + if err == nil { + t.Fatal("first Get: expected NOTFOUND error, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Get: expected NOTFOUND, got %v", err) + } + if wrapper.getCallCount.Load() != 1 { + t.Errorf("first Get: expected 1 Get call, got %d", wrapper.getCallCount.Load()) + } + + // Second Get: should hit cache, wrapper not called again + item, err = adapter.Get(ctx, scope, "query1", false) + if item != nil { + t.Errorf("second Get: expected nil item, got %v", item) + } + if err == nil { + t.Fatal("second Get: expected NOTFOUND error, got nil") + } + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Get: expected NOTFOUND, got %v", err) + } + if wrapper.getCallCount.Load() != 1 { + t.Errorf("second Get: expected still 1 Get call (cache hit), got %d", wrapper.getCallCount.Load()) + } +} + +// TestListEmptyCachesNotFound tests that when wrapper List returns ([], nil), the adapter +// caches NOTFOUND and a second List returns empty from cache without calling the wrapper again. +func TestListEmptyCachesNotFound(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + // Use AWS item type so adapter validation does not require GCP predefined role. + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache).(interface { + List(context.Context, string, bool) ([]*sdp.Item, error) + }) + + // First List: miss, wrapper returns ([], nil), adapter caches NOTFOUND + items, err := adapter.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List: unexpected error %v", err) + } + if items == nil { + t.Error("first List: expected non-nil empty slice, got nil") + } + if len(items) != 0 { + t.Errorf("first List: expected 0 items, got %d", len(items)) + } + if wrapper.listCallCount.Load() != 1 { + t.Errorf("first List: expected 1 List call, got %d", wrapper.listCallCount.Load()) + } + + // Second List: should hit cache, wrapper not called again + items, err = adapter.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List: unexpected error %v", err) + } + if items == nil { + t.Error("second List: expected non-nil empty slice, got nil") + } + if len(items) != 0 { + t.Errorf("second List: expected 0 items, got %d", len(items)) + } + if wrapper.listCallCount.Load() != 1 { + t.Errorf("second List: expected still 1 List call (cache hit), got %d", wrapper.listCallCount.Load()) + } +} + +// TestSearchEmptyCachesNotFound tests that when wrapper Search returns ([], nil), the adapter +// caches NOTFOUND and a second Search returns empty from cache without calling the wrapper again. +func TestSearchEmptyCachesNotFound(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + // Use AWS item type so adapter validation does not require GCP predefined role. + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache).(interface { + Search(context.Context, string, string, bool) ([]*sdp.Item, error) + }) + + query := "id1" + + // First Search: miss, wrapper returns ([], nil), adapter caches NOTFOUND + items, err := adapter.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error %v", err) + } + if items == nil { + t.Error("first Search: expected non-nil empty slice, got nil") + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + if wrapper.searchCallCount.Load() != 1 { + t.Errorf("first Search: expected 1 Search call, got %d", wrapper.searchCallCount.Load()) + } + + // Second Search: should hit cache, wrapper not called again + items, err = adapter.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error %v", err) + } + if items == nil { + t.Error("second Search: expected non-nil empty slice, got nil") + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + if wrapper.searchCallCount.Load() != 1 { + t.Errorf("second Search: expected still 1 Search call (cache hit), got %d", wrapper.searchCallCount.Load()) + } +} + +// TestGetNOTFOUNDCacheHitMatchesLiveNOTFOUND asserts response parity: a NOTFOUND cache hit returns +// the same (item, error) as a fresh NOTFOUND — nil item and identical error type and error message. +func TestGetNOTFOUNDCacheHitMatchesLiveNOTFOUND(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache) + + query := "query1" + // Live NOTFOUND + liveItem, liveErr := adapter.Get(ctx, scope, query, false) + // Cache NOTFOUND (second call hits cache) + cacheItem, cacheErr := adapter.Get(ctx, scope, query, false) + + // Same item: both nil + if liveItem != nil || cacheItem != nil { + t.Errorf("both responses must have nil item: live=%v cache=%v", liveItem, cacheItem) + } + // Same error semantics: both NOTFOUND with same message + var liveQE, cacheQE *sdp.QueryError + if !errors.As(liveErr, &liveQE) || !errors.As(cacheErr, &cacheQE) { + t.Fatalf("both errors must be QueryError: live=%v cache=%v", liveErr, cacheErr) + } + if liveQE.GetErrorType() != sdp.QueryError_NOTFOUND || cacheQE.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("both must be NOTFOUND: live=%v cache=%v", liveQE.GetErrorType(), cacheQE.GetErrorType()) + } + if liveQE.GetErrorString() != cacheQE.GetErrorString() { + t.Errorf("error string must match: live=%q cache=%q", liveQE.GetErrorString(), cacheQE.GetErrorString()) + } +} + +// TestListNOTFOUNDCacheHitMatchesLiveNOTFOUND asserts response parity: a NOTFOUND cache hit for List +// returns the same (items, error) as a fresh not-found — empty slice and nil error. +func TestListNOTFOUNDCacheHitMatchesLiveNOTFOUND(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache).(interface { + List(context.Context, string, bool) ([]*sdp.Item, error) + }) + + liveItems, liveErr := adapter.List(ctx, scope, false) + cacheItems, cacheErr := adapter.List(ctx, scope, false) + + if liveErr != nil || cacheErr != nil { + t.Errorf("both must return nil error: live=%v cache=%v", liveErr, cacheErr) + } + if liveItems == nil || cacheItems == nil { + t.Errorf("both must return non-nil slice: live=%v cache=%v", liveItems, cacheItems) + } + if len(liveItems) != 0 || len(cacheItems) != 0 { + t.Errorf("both must return empty slice: live len=%d cache len=%d", len(liveItems), len(cacheItems)) + } +} + +// TestSearchNOTFOUNDCacheHitMatchesLiveNOTFOUND asserts response parity: a NOTFOUND cache hit for Search +// returns the same (items, error) as a fresh not-found — empty slice and nil error. +func TestSearchNOTFOUNDCacheHitMatchesLiveNOTFOUND(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache).(interface { + Search(context.Context, string, string, bool) ([]*sdp.Item, error) + }) + + query := "id1" + liveItems, liveErr := adapter.Search(ctx, scope, query, false) + cacheItems, cacheErr := adapter.Search(ctx, scope, query, false) + + if liveErr != nil || cacheErr != nil { + t.Errorf("both must return nil error: live=%v cache=%v", liveErr, cacheErr) + } + if liveItems == nil || cacheItems == nil { + t.Errorf("both must return non-nil slice: live=%v cache=%v", liveItems, cacheItems) + } + if len(liveItems) != 0 || len(cacheItems) != 0 { + t.Errorf("both must return empty slice: live len=%d cache len=%d", len(liveItems), len(cacheItems)) + } +} diff --git a/stdlib-source/adapters/certificate.go b/stdlib-source/adapters/certificate.go index 406af2f2..018c2c5e 100644 --- a/stdlib-source/adapters/certificate.go +++ b/stdlib-source/adapters/certificate.go @@ -11,7 +11,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // CertToName Returns the name of a cert as a string. This is in the format of: @@ -252,12 +252,6 @@ func (s *CertificateAdapter) Search(ctx context.Context, scope string, query str Query: cert.Issuer.String(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing issuer will affect the child - In: true, - // The child can't affect the issuer - Out: false, - }, }) } } diff --git a/stdlib-source/adapters/certificate_test.go b/stdlib-source/adapters/certificate_test.go index 976e6e81..871fda7c 100644 --- a/stdlib-source/adapters/certificate_test.go +++ b/stdlib-source/adapters/certificate_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" ) var chain = `-----BEGIN CERTIFICATE----- diff --git a/stdlib-source/adapters/dns.go b/stdlib-source/adapters/dns.go index c5811aa6..3d7879fe 100644 --- a/stdlib-source/adapters/dns.go +++ b/stdlib-source/adapters/dns.go @@ -11,8 +11,8 @@ import ( "github.com/cenkalti/backoff/v5" "github.com/miekg/dns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) @@ -41,6 +41,10 @@ func NewDNSAdapterForHealthCheck() *DNSAdapter { const dnsCacheDuration = 5 * time.Minute +// maxOperationTimeout is the maximum time any single DNS Get/Search operation can take. +// This prevents slow DNS queries from degrading overall system performance. +const maxOperationTimeout = 30 * time.Second + var DefaultServers = []string{ "169.254.169.253:53", // Route 53 default resolver. See https://docs.aws.amazon.com/vpc/latest/userguide/AmazonDNS-concepts.html#AmazonDNS "1.1.1.1:53", @@ -102,8 +106,13 @@ func (d *DNSAdapter) Scopes() []string { } } -// Gets a single item. This expects a DNS name +// Get retrieves a single DNS item by name. +// The operation is capped at maxOperationTimeout (30s) regardless of the caller's context deadline. func (d *DNSAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { + // Enforce maximum timeout for this operation + ctx, cancel := context.WithTimeout(ctx, maxOperationTimeout) + defer cancel() + if scope != "global" { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -149,17 +158,25 @@ func (d *DNSAdapter) Get(ctx context.Context, scope string, query string, ignore // should be using Search() now anyway items, err := d.MakeQuery(ctx, query) if err != nil { + // makeQueryImpl returns NOTFOUND when no A/AAAA records exist; cache it to avoid repeated lookups + var qe *sdp.QueryError + if errors.As(err, &qe) && qe.GetErrorType() == sdp.QueryError_NOTFOUND { + d.cache.StoreError(ctx, qe, dnsCacheDuration, ck) + } return nil, err } if len(items) == 0 { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "no DNS records found", - Scope: scope, - SourceName: d.Name(), - ItemType: d.Type(), + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no DNS records found", + Scope: scope, + SourceName: d.Name(), + ItemType: d.Type(), + ResponderName: d.Name(), } + d.cache.StoreError(ctx, notFoundErr, dnsCacheDuration, ck) + return nil, notFoundErr } d.cache.StoreItem(ctx, items[0], dnsCacheDuration, ck) return items[0], nil @@ -186,12 +203,20 @@ type DNSRecord struct { Type string } +// Search performs a DNS lookup for a name or reverse lookup for an IP. +// The operation is capped at maxOperationTimeout (30s) regardless of the caller's context deadline. func (d *DNSAdapter) Search(ctx context.Context, scope string, query string, ignoreCache bool) ([]*sdp.Item, error) { + // Enforce maximum timeout for this operation + ctx, cancel := context.WithTimeout(ctx, maxOperationTimeout) + defer cancel() + if scope != "global" { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, ErrorString: "DNS queries only supported in global scope", Scope: scope, + SourceName: d.Name(), + ItemType: d.Type(), } } @@ -205,6 +230,7 @@ func (d *DNSAdapter) Search(ctx context.Context, scope string, query string, ign cacheHit, _, cachedItems, qErr, done = d.cache.Lookup(ctx, d.Name(), sdp.QueryMethod_SEARCH, scope, d.Type(), query, ignoreCache) defer done() if qErr != nil { + // Cached NOTFOUND: return same (nil, error) as fresh lookup for consistency return nil, qErr } if cacheHit { @@ -220,18 +246,23 @@ func (d *DNSAdapter) Search(ctx context.Context, scope string, query string, ign // If it's an IP then we want to run a reverse lookup items, err := d.MakeReverseQuery(ctx, query) if err != nil { - d.cache.StoreError(ctx, err, dnsCacheDuration, ck) + // Only cache NOTFOUND to avoid repeated lookups; do not cache transient errors (e.g. timeouts). + var qe *sdp.QueryError + if errors.As(err, &qe) && qe.GetErrorType() == sdp.QueryError_NOTFOUND { + d.cache.StoreError(ctx, err, dnsCacheDuration, ck) + } return nil, err } if len(items) == 0 { - // Cache NOTFOUND error for empty results to avoid repeated network calls + // Cache NOTFOUND for empty results; return (nil, error) so cache hit returns same as fresh. notFoundErr := &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "no reverse DNS records found", - Scope: "global", - SourceName: d.Name(), - ItemType: d.Type(), + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no reverse DNS records found", + Scope: "global", + SourceName: d.Name(), + ItemType: d.Type(), + ResponderName: d.Name(), } d.cache.StoreError(ctx, notFoundErr, dnsCacheDuration, ck) return nil, notFoundErr @@ -251,9 +282,14 @@ func (d *DNSAdapter) Search(ctx context.Context, scope string, query string, ign items, err := d.MakeQuery(ctx, query) if err != nil { - d.cache.StoreError(ctx, err, dnsCacheDuration, ck) + // Only cache NOTFOUND to avoid repeated lookups; return (nil, error) so cache hit returns same as fresh. + var qe *sdp.QueryError + if errors.As(err, &qe) && qe.GetErrorType() == sdp.QueryError_NOTFOUND { + d.cache.StoreError(ctx, err, dnsCacheDuration, ck) + } return nil, err } + // MakeQuery never returns (nil, 0 items): makeQueryImpl returns NOTFOUND when there are no A/AAAA answers, and when there are answers it only groups CNAME/A/AAAA so at least one item is produced. for _, item := range items { d.cache.StoreItem(ctx, item, dnsCacheDuration, ck) @@ -469,10 +505,6 @@ func (d *DNSAdapter) makeQueryImpl(ctx context.Context, query string, server str Query: name, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -566,11 +598,6 @@ func AToItem(name string, records []dns.RR) (*sdp.Item, error) { Query: ip.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -604,12 +631,6 @@ func AToItem(name string, records []dns.RR) (*sdp.Item, error) { Query: name, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the domain will affect the DNS entry - In: true, - // Changes to the DNS entry won't affect the domain - Out: false, - }, }) return &item, nil diff --git a/stdlib-source/adapters/dns_test.go b/stdlib-source/adapters/dns_test.go index ca72183d..091c880f 100644 --- a/stdlib-source/adapters/dns_test.go +++ b/stdlib-source/adapters/dns_test.go @@ -5,10 +5,11 @@ import ( "errors" "net" "testing" + "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSearch(t *testing.T) { @@ -24,9 +25,12 @@ func TestSearch(t *testing.T) { t.Run("with a bad DNS name", func(t *testing.T) { _, err := s.Search(context.Background(), "global", "not.real.overmind.tech", false) - if err == nil { - t.Error("expected error") + t.Error("expected error for non-existent name") + } + var qe *sdp.QueryError + if !errors.As(err, &qe) || qe.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND error, got %v", err) } }) @@ -65,6 +69,40 @@ func TestSearch(t *testing.T) { discovery.TestValidateItems(t, items) }) + t.Run("Search returns same NOTFOUND for first and second call", func(t *testing.T) { + // First call (fresh NOTFOUND) and second call (cached NOTFOUND) must return the same: nil items, same error + cache := sdpcache.NewMemoryCache() + cachedSrc := DNSAdapter{cache: cache, Servers: s.Servers} + query := "not.real.overmind.tech" + + first, err1 := cachedSrc.Search(context.Background(), "global", query, false) + if err1 == nil { + t.Fatal("first Search: expected NOTFOUND error, got nil") + } + if first != nil { + t.Errorf("first Search: expected nil items, got len=%d", len(first)) + } + var qe *sdp.QueryError + if !errors.As(err1, &qe) || qe.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Search: expected NOTFOUND, got %v", err1) + } + firstErrStr := err1.Error() + + second, err2 := cachedSrc.Search(context.Background(), "global", query, false) + if err2 == nil { + t.Fatal("second Search: expected NOTFOUND error, got nil") + } + if second != nil { + t.Errorf("second Search: expected nil items, got len=%d", len(second)) + } + if !errors.As(err2, &qe) || qe.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Search: expected NOTFOUND, got %v", err2) + } + if err2.Error() != firstErrStr { + t.Errorf("first and second Search must return same error message: first %q, second %q", firstErrStr, err2.Error()) + } + }) + t.Run("with an IP and therefore reverse DNS", func(t *testing.T) { s.ReverseLookup = true items, err := s.Search(context.Background(), "global", "1.1.1.1", false) @@ -141,6 +179,52 @@ func TestDnsGet(t *testing.T) { } }) + t.Run("GET returns NOTFOUND when cache has NOTFOUND", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + cachedSrc := DNSAdapter{cache: cache} + query := "cached.notfound.get.example" + + // Pre-seed cache with NOTFOUND (simulates a previous Get that got 0 records) + ck := sdpcache.CacheKeyFromParts(cachedSrc.Name(), sdp.QueryMethod_GET, "global", cachedSrc.Type(), query) + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no DNS records found", + Scope: "global", + SourceName: cachedSrc.Name(), + ItemType: cachedSrc.Type(), + } + cache.StoreError(context.Background(), notFoundErr, dnsCacheDuration, ck) + + // Get should return cached NOTFOUND without doing a DNS lookup + item, err := cachedSrc.Get(context.Background(), "global", query, false) + if item != nil { + t.Errorf("expected nil item, got %v", item) + } + if err == nil { + t.Fatal("expected NOTFOUND error, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND, got %v", err) + } + + // Second Get: should still return cached NOTFOUND (same response as first) + firstErrStr := err.Error() + item, err = cachedSrc.Get(context.Background(), "global", query, false) + if item != nil { + t.Errorf("second Get: expected nil item, got %v", item) + } + if err == nil { + t.Fatal("second Get: expected NOTFOUND error, got nil") + } + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Get: expected NOTFOUND, got %v", err) + } + if err.Error() != firstErrStr { + t.Errorf("first and second Get must return same error message: first %q, second %q", firstErrStr, err.Error()) + } + }) + t.Run("bad scope", func(t *testing.T) { _, err := src.Get(context.Background(), "something.local.test", "something.does.not.exist.please.testing", false) @@ -171,3 +255,123 @@ func TestDnsGet(t *testing.T) { t.Log(item) }) } + +// TestGetTimeout verifies that Get enforces the maximum timeout by checking +// that the adapter's timeout takes precedence over a longer caller timeout +func TestGetTimeout(t *testing.T) { + if testing.Short() { + t.Skip("Skipping timeout test in short mode") + } + + src := DNSAdapter{ + cache: sdpcache.NewNoOpCache(), + // Use a non-existent DNS server to force timeout + Servers: []string{"192.0.2.1:53"}, // TEST-NET-1, guaranteed to be unroutable + } + + // Create a context with a very long deadline to verify adapter's internal timeout takes precedence + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + start := time.Now() + _, err := src.Get(ctx, "global", "test.example.com", false) + elapsed := time.Since(start) + + // The operation should fail (no response from DNS server) + if err == nil { + t.Error("expected error but got nil") + } + + // The operation should complete around the maxOperationTimeout (30s), not the caller's 10 minutes + // Allow generous buffer for CI variance and different network behaviors + if elapsed > 35*time.Second { + t.Errorf("Get took %v, expected around 30s (max 35s for variance), timeout may not be properly enforced", elapsed) + } + + // Don't assert minimum duration as TEST-NET may fail fast in some environments + // The key assertion is that it completes in ~30s, not 10 minutes +} + +// TestSearchTimeoutContext verifies that Search properly wraps the context with a timeout +func TestSearchTimeoutContext(t *testing.T) { + t.Parallel() + + src := DNSAdapter{ + cache: sdpcache.NewNoOpCache(), + } + + // Create a context with a very long deadline to ensure Search creates its own timeout + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Use a valid, fast DNS query to verify the timeout wrapper doesn't break normal operation + items, err := src.Search(ctx, "global", "one.one.one.one", false) + + // Should succeed with the fast query + if err != nil { + t.Errorf("expected no error for valid query, got: %v", err) + } + + // Should return at least one item for this known DNS name + if len(items) == 0 { + t.Error("expected at least one DNS item for one.one.one.one") + } +} + +// TestListBehavior verifies that List returns an empty slice without making DNS queries +func TestListBehavior(t *testing.T) { + t.Parallel() + + src := DNSAdapter{ + cache: sdpcache.NewNoOpCache(), + } + + ctx := context.Background() + + // List should return an empty slice without making any DNS queries + items, err := src.List(ctx, "global", false) + + // List should succeed with empty results + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + + if len(items) != 0 { + t.Errorf("expected empty list, got %d items", len(items)) + } +} + +// TestTimeoutShorterThanCaller verifies that a short caller timeout is respected +func TestTimeoutShorterThanCaller(t *testing.T) { + t.Parallel() + + src := DNSAdapter{ + cache: sdpcache.NewNoOpCache(), + // Use a non-existent DNS server to force timeout + Servers: []string{"192.0.2.1:53"}, // TEST-NET-1, guaranteed to be unroutable + } + + // Create a context with a 2s deadline (shorter than the adapter's 30s max) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + start := time.Now() + _, err := src.Get(ctx, "global", "test.example.com", false) + elapsed := time.Since(start) + + // The operation should fail (no response from DNS server) + if err == nil { + t.Error("expected error but got nil") + } + + // The operation should complete in roughly 2 seconds (the caller's timeout), not 30s + // Allow some buffer for processing time (4s max) + if elapsed > 4*time.Second { + t.Errorf("Get took %v, expected around 2s (max 4s)", elapsed) + } + + // Verify it's a context deadline exceeded error + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("expected context.DeadlineExceeded error, got: %v", err) + } +} diff --git a/stdlib-source/adapters/http.go b/stdlib-source/adapters/http.go index 90ec4aef..4db87391 100644 --- a/stdlib-source/adapters/http.go +++ b/stdlib-source/adapters/http.go @@ -12,8 +12,8 @@ import ( "strings" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) @@ -175,11 +175,11 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor return nil, qErr } if cacheHit { + // Get only caches a single item or NOTFOUND (via StoreError). Guard against empty slice for defensive safety (e.g. cache corruption). if len(cachedItems) > 0 { return cachedItems[0], nil - } else { - return nil, nil } + return nil, nil } // Create a client that skips TLS verification since we will want to get the @@ -228,6 +228,21 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor // Clean up connections once we're done defer client.CloseIdleConnections() + defer res.Body.Close() + + // Treat HTTP 404 and 410 as not-found; cache to avoid repeated requests. + if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusGone { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("HTTP %s for %s", res.Status, query), + Scope: scope, + SourceName: s.Name(), + ItemType: s.Type(), + ResponderName: s.Name(), + } + s.cache.StoreError(ctx, notFoundErr, httpCacheDuration, ck) + return nil, notFoundErr + } // Convert headers from map[string][]string to map[string]string. This means // that headers that were returned many times will end up with their values @@ -274,11 +289,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor Query: ip.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always linked - In: true, - Out: true, - }, }) } else { // If the host is not an ip, try to resolve via DNS @@ -289,11 +299,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor Query: req.URL.Hostname(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always linked - In: true, - Out: true, - }, }) } @@ -340,12 +345,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor Query: strings.Join(certs, "\n"), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the cert will affect the HTTP endpoint - In: true, - // The HTTP endpoint won't affect the cert - Out: false, - }, }) } } @@ -391,11 +390,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor Query: resolvedURL.String(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Redirects are tightly coupled - In: true, - Out: true, - }, }) } } @@ -434,13 +428,13 @@ func (s *HTTPAdapter) Search(ctx context.Context, scope string, query string, ig // Use the existing Get method to retrieve the item item, err := s.Get(ctx, scope, cleanURL, ignoreCache) if err != nil { + // Return (nil, error) for NOTFOUND so cache hit and fresh lookup behave the same return nil, err } - if item == nil { + // Get can return (nil, nil) on the defensive path when cache reports hit but cachedItems is empty (e.g. cache corruption). return []*sdp.Item{}, nil } - return []*sdp.Item{item}, nil } diff --git a/stdlib-source/adapters/http_test.go b/stdlib-source/adapters/http_test.go index 362b8ff0..5370cd79 100644 --- a/stdlib-source/adapters/http_test.go +++ b/stdlib-source/adapters/http_test.go @@ -11,9 +11,9 @@ import ( "testing" "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) const TestHTTPTimeout = 3 * time.Second @@ -126,7 +126,9 @@ func TestHTTPGet(t *testing.T) { defer server.TLSServer.Close() t.Run("With a specified port and dns name", func(t *testing.T) { - item, err := src.Get(context.Background(), "global", "https://"+net.JoinHostPort("localhost", server.Port), false) + // Use localhost with /200 so we get an item and exercise DNS link; root path returns 404 which we now treat as NOTFOUND + url := fmt.Sprintf("https://localhost:%s/200", server.Port) + item, err := src.Get(context.Background(), "global", url, false) if err != nil { t.Fatal(err) } @@ -178,23 +180,114 @@ func TestHTTPGet(t *testing.T) { }) t.Run("With a 404", func(t *testing.T) { + // 404 is cached as NOTFOUND; no item returned item, err := src.Get(context.Background(), "global", server.NotFoundPage, false) - if err != nil { - t.Fatal(err) + if item != nil { + t.Errorf("expected nil item for 404, got %v", item) + } + if err == nil { + t.Fatal("expected NOTFOUND error for 404, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND error for 404, got %v", err) } + }) - var status interface{} + t.Run("404 NOTFOUND is cached and second Get does not hit server", func(t *testing.T) { + var count int + mux := http.NewServeMux() + mux.HandleFunc("/404", func(w http.ResponseWriter, _ *http.Request) { + count++ + w.WriteHeader(http.StatusNotFound) + }) + srv := httptest.NewTLSServer(mux) + defer srv.Close() - status, err = item.GetAttributes().Get("status") - if err != nil { - t.Fatal(err) + cachedSrc := HTTPAdapter{cache: sdpcache.NewMemoryCache()} + url404 := srv.URL + "/404" + + // First call: 404 is cached as NOTFOUND + item, err := cachedSrc.Get(context.Background(), "global", url404, false) + if item != nil { + t.Errorf("first Get: expected nil item, got %v", item) + } + if err == nil { + t.Fatal("first Get: expected NOTFOUND error, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Get: expected NOTFOUND, got %v", err) + } + if count != 1 { + t.Errorf("first Get: expected 1 request, got %d", count) + } + firstErrStr := err.Error() + + // Second call: should hit cache, no new request; same response as first (nil item, NOTFOUND, same message) + item, err = cachedSrc.Get(context.Background(), "global", url404, false) + if item != nil { + t.Errorf("second Get: expected nil item, got %v", item) } + if err == nil { + t.Fatal("second Get: expected NOTFOUND error, got nil") + } + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Get: expected NOTFOUND, got %v", err) + } + if err.Error() != firstErrStr { + t.Errorf("first and second Get must return same error message: first %q, second %q", firstErrStr, err.Error()) + } + if count != 1 { + t.Errorf("second Get: expected no new request (count still 1), got %d", count) + } + }) + + t.Run("Search 404 returns same NOTFOUND for first and second call", func(t *testing.T) { + var count int + mux := http.NewServeMux() + mux.HandleFunc("/404", func(w http.ResponseWriter, _ *http.Request) { + count++ + w.WriteHeader(http.StatusNotFound) + }) + srv := httptest.NewTLSServer(mux) + defer srv.Close() - if status != float64(404) { - t.Errorf("expected status to be 404, got: %v", status) + cachedSrc := HTTPAdapter{cache: sdpcache.NewMemoryCache()} + url404 := srv.URL + "/404" + + first, err1 := cachedSrc.Search(context.Background(), "global", url404, false) + if err1 == nil { + t.Fatal("first Search: expected NOTFOUND error, got nil") + } + if first != nil { + t.Errorf("first Search: expected nil items, got len=%d", len(first)) } + var qErr *sdp.QueryError + if !errors.As(err1, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Search: expected NOTFOUND, got %v", err1) + } + if count != 1 { + t.Errorf("first Search: expected 1 request, got %d", count) + } + firstErrStr := err1.Error() - discovery.TestValidateItem(t, item) + second, err2 := cachedSrc.Search(context.Background(), "global", url404, false) + if err2 == nil { + t.Fatal("second Search: expected NOTFOUND error, got nil") + } + if second != nil { + t.Errorf("second Search: expected nil items, got len=%d", len(second)) + } + if !errors.As(err2, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Search: expected NOTFOUND, got %v", err2) + } + if err2.Error() != firstErrStr { + t.Errorf("first and second Search must return same error message: first %q, second %q", firstErrStr, err2.Error()) + } + if count != 1 { + t.Errorf("second Search: expected no new request (count still 1), got %d", count) + } }) t.Run("With a timeout", func(t *testing.T) { diff --git a/stdlib-source/adapters/ip.go b/stdlib-source/adapters/ip.go index fa5d6982..dfc1d419 100644 --- a/stdlib-source/adapters/ip.go +++ b/stdlib-source/adapters/ip.go @@ -5,7 +5,7 @@ import ( "fmt" "net" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // IPAdapter struct on which all methods are registered @@ -159,11 +159,6 @@ func (bc *IPAdapter) Get(ctx context.Context, scope string, query string, ignore Query: ip.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always linked - In: true, - Out: true, - }, }, { // RDAP @@ -173,11 +168,6 @@ func (bc *IPAdapter) Get(ctx context.Context, scope string, query string, ignore Query: ip.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // do not link through rdap definitions to avoid huge blast radius - In: false, - Out: false, - }, }, }, }, nil diff --git a/stdlib-source/adapters/ip_test.go b/stdlib-source/adapters/ip_test.go index 05ceee6e..89e7ee52 100644 --- a/stdlib-source/adapters/ip_test.go +++ b/stdlib-source/adapters/ip_test.go @@ -5,8 +5,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" ) func TestIPGet(t *testing.T) { diff --git a/stdlib-source/adapters/main.go b/stdlib-source/adapters/main.go index a85612eb..1c8ddfa0 100644 --- a/stdlib-source/adapters/main.go +++ b/stdlib-source/adapters/main.go @@ -9,9 +9,9 @@ import ( "time" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/stdlib-source/adapters/test" _ "embed" @@ -131,12 +131,6 @@ func extractEntityLinks(entities []rdap.Entity) []*sdp.LinkedItemQuery { Query: selfLink, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // The Entity isn't a "real" component, so no matter what - // changes it won't actually "affect" anything - In: false, - Out: false, - }, }) } } diff --git a/stdlib-source/adapters/rdap-asn.go b/stdlib-source/adapters/rdap-asn.go index b60f21ee..dd41e7b9 100644 --- a/stdlib-source/adapters/rdap-asn.go +++ b/stdlib-source/adapters/rdap-asn.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapASNAdapter struct { diff --git a/stdlib-source/adapters/rdap-asn_test.go b/stdlib-source/adapters/rdap-asn_test.go index 51065960..6559d922 100644 --- a/stdlib-source/adapters/rdap-asn_test.go +++ b/stdlib-source/adapters/rdap-asn_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestASNAdapterGet(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-domain.go b/stdlib-source/adapters/rdap-domain.go index 2fe4421c..1502e069 100644 --- a/stdlib-source/adapters/rdap-domain.go +++ b/stdlib-source/adapters/rdap-domain.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapDomainAdapter struct { @@ -197,12 +197,6 @@ func (s *RdapDomainAdapter) Search(ctx context.Context, scope string, query stri Query: newURL.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // A change in a name server could affect the domains - In: true, - // Domains won't affect the name server - Out: false, - }, }) } @@ -221,11 +215,6 @@ func (s *RdapDomainAdapter) Search(ctx context.Context, scope string, query stri Query: network.StartAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // do not link through rdap definitions to avoid huge blast radius - In: false, - Out: false, - }, }) } diff --git a/stdlib-source/adapters/rdap-domain_test.go b/stdlib-source/adapters/rdap-domain_test.go index 27fe878e..62260794 100644 --- a/stdlib-source/adapters/rdap-domain_test.go +++ b/stdlib-source/adapters/rdap-domain_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDomainAdapterGet(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-entity.go b/stdlib-source/adapters/rdap-entity.go index f8deb17c..9cb30028 100644 --- a/stdlib-source/adapters/rdap-entity.go +++ b/stdlib-source/adapters/rdap-entity.go @@ -6,8 +6,8 @@ import ( "net/url" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapEntityAdapter struct { @@ -188,13 +188,6 @@ func (s *RdapEntityAdapter) runEntityRequest(ctx context.Context, query string, Query: autnum.Handle, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The ASN won't affect the entity - In: false, - // The entity could maybe affect the ASN? Change this if it - // causes issues - Out: true, - }, }) } diff --git a/stdlib-source/adapters/rdap-entity_test.go b/stdlib-source/adapters/rdap-entity_test.go index eb90b64b..2c37be37 100644 --- a/stdlib-source/adapters/rdap-entity_test.go +++ b/stdlib-source/adapters/rdap-entity_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestEntityAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-ip-network.go b/stdlib-source/adapters/rdap-ip-network.go index 94258234..212b55a8 100644 --- a/stdlib-source/adapters/rdap-ip-network.go +++ b/stdlib-source/adapters/rdap-ip-network.go @@ -6,8 +6,8 @@ import ( "net" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapIPNetworkAdapter struct { diff --git a/stdlib-source/adapters/rdap-ip-network_test.go b/stdlib-source/adapters/rdap-ip-network_test.go index b45b91af..2bb29fe0 100644 --- a/stdlib-source/adapters/rdap-ip-network_test.go +++ b/stdlib-source/adapters/rdap-ip-network_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestIpNetworkAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-nameserver.go b/stdlib-source/adapters/rdap-nameserver.go index e5cf2b53..0d0f052f 100644 --- a/stdlib-source/adapters/rdap-nameserver.go +++ b/stdlib-source/adapters/rdap-nameserver.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapNameserverAdapter struct { @@ -177,11 +177,6 @@ func (s *RdapNameserverAdapter) Search(ctx context.Context, scope string, query Query: strings.ToLower(nameserver.LDHName), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These represent the same thing so linked them both ways - In: true, - Out: true, - }, }) // Link IP addresses @@ -196,11 +191,6 @@ func (s *RdapNameserverAdapter) Search(ctx context.Context, scope string, query Query: ip, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } diff --git a/stdlib-source/adapters/rdap-nameserver_test.go b/stdlib-source/adapters/rdap-nameserver_test.go index 46882a5d..ebfdd282 100644 --- a/stdlib-source/adapters/rdap-nameserver_test.go +++ b/stdlib-source/adapters/rdap-nameserver_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNameserverAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/test/data.go b/stdlib-source/adapters/test/data.go index f286f7a1..4f736554 100644 --- a/stdlib-source/adapters/test/data.go +++ b/stdlib-source/adapters/test/data.go @@ -5,7 +5,7 @@ import ( "sync/atomic" "time" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -100,11 +100,6 @@ func admins() *sdp.Item { Query: "test-dylan", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // the show must go on - In: false, - Out: false, - }, }, } @@ -121,11 +116,6 @@ func dylan() *sdp.Item { Method: sdp.QueryMethod_LIST, Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // best friends - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -134,12 +124,6 @@ func dylan() *sdp.Item { Query: "test-motorcycling", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // accidents happen - In: true, - // motorcycles will endure - Out: false, - }, }, { Query: &sdp.Query{ @@ -148,12 +132,6 @@ func dylan() *sdp.Item { Query: "test-london", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // we are what we eat - In: true, - // london don't care - Out: false, - }, }, } @@ -171,12 +149,6 @@ func manny() *sdp.Item { Query: "test-london", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // we are what we eat - In: true, - // london don't care - Out: false, - }, }, { Query: &sdp.Query{ @@ -185,12 +157,6 @@ func manny() *sdp.Item { Query: "test-kibble", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // there are other options - In: false, - // the kibble is soon gone - Out: true, - }, }, } @@ -219,11 +185,6 @@ func london() *sdp.Item { Query: "test-gb", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // politics, enough said - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -232,10 +193,6 @@ func london() *sdp.Item { Query: "*", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: false, - }, }, { Query: &sdp.Query{ @@ -244,10 +201,6 @@ func london() *sdp.Item { Query: "test-soho", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/stdlib-source/adapters/test/testdog.go b/stdlib-source/adapters/test/testdog.go index a18ea676..225def11 100644 --- a/stdlib-source/adapters/test/testdog.go +++ b/stdlib-source/adapters/test/testdog.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestDogAdapter An adapter of `dog` items for automated tests. diff --git a/stdlib-source/adapters/test/testfood.go b/stdlib-source/adapters/test/testfood.go index 8b8611e8..4d7e2366 100644 --- a/stdlib-source/adapters/test/testfood.go +++ b/stdlib-source/adapters/test/testfood.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestFoodAdapter A adapter of `food` items for automated tests. diff --git a/stdlib-source/adapters/test/testgroup.go b/stdlib-source/adapters/test/testgroup.go index 3678a380..af73941d 100644 --- a/stdlib-source/adapters/test/testgroup.go +++ b/stdlib-source/adapters/test/testgroup.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestGroupAdapter A adapter of `group` items for automated tests. diff --git a/stdlib-source/adapters/test/testhobby.go b/stdlib-source/adapters/test/testhobby.go index 3b2c10d8..43ea989b 100644 --- a/stdlib-source/adapters/test/testhobby.go +++ b/stdlib-source/adapters/test/testhobby.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestHobbyAdapter A adapter of `hobby` items for automated tests. diff --git a/stdlib-source/adapters/test/testlocation.go b/stdlib-source/adapters/test/testlocation.go index 33ae082e..cf487d72 100644 --- a/stdlib-source/adapters/test/testlocation.go +++ b/stdlib-source/adapters/test/testlocation.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestLocationAdapter A adapter of `location` items for automated tests. diff --git a/stdlib-source/adapters/test/testperson.go b/stdlib-source/adapters/test/testperson.go index 604591e7..d043e2e5 100644 --- a/stdlib-source/adapters/test/testperson.go +++ b/stdlib-source/adapters/test/testperson.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestPersonAdapter A adapter of `person` items for automated tests. diff --git a/stdlib-source/adapters/test/testregion.go b/stdlib-source/adapters/test/testregion.go index bf4432e2..42e49ac3 100644 --- a/stdlib-source/adapters/test/testregion.go +++ b/stdlib-source/adapters/test/testregion.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestRegionAdapter A adapter of `region` items for automated tests. diff --git a/stdlib-source/build/package/Dockerfile b/stdlib-source/build/package/Dockerfile index 7bd7e09b..deb0f149 100644 --- a/stdlib-source/build/package/Dockerfile +++ b/stdlib-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source stdlib-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source stdlib-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/stdlib-source/cmd/root.go b/stdlib-source/cmd/root.go index b65ecfe1..d2e37d6d 100644 --- a/stdlib-source/cmd/root.go +++ b/stdlib-source/cmd/root.go @@ -9,10 +9,10 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/logging" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/logging" "github.com/overmindtech/cli/stdlib-source/adapters" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/cli/go/tracing" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/tfutils/plan_mapper.go b/tfutils/plan_mapper.go index f0b2311c..d79409eb 100644 --- a/tfutils/plan_mapper.go +++ b/tfutils/plan_mapper.go @@ -13,8 +13,9 @@ import ( "github.com/google/uuid" awsAdapters "github.com/overmindtech/cli/aws-source/adapters" k8sAdapters "github.com/overmindtech/cli/k8s-source/adapters" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpAdapters "github.com/overmindtech/cli/sources/gcp/proc" + azureAdapters "github.com/overmindtech/cli/sources/azure/proc" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -163,6 +164,7 @@ func MappedItemDiffsFromPlan(ctx context.Context, planJson []byte, fileName stri adapterMetadata := awsAdapters.Metadata.AllAdapterMetadata() adapterMetadata = append(adapterMetadata, k8sAdapters.Metadata.AllAdapterMetadata()...) adapterMetadata = append(adapterMetadata, gcpAdapters.Metadata.AllAdapterMetadata()...) + adapterMetadata = append(adapterMetadata, azureAdapters.Metadata.AllAdapterMetadata()...) // These mappings are from the terraform type, to required mapping data mappings := make(map[string][]TfMapData) for _, metadata := range adapterMetadata { @@ -342,7 +344,7 @@ func mapResourceToQuery(itemDiff *sdp.ItemDiff, terraformResource *Resource, map // If we get to this point, we haven't found a mapping message := fmt.Sprintf("missing mapping attribute: %v", strings.Join(attemptedMappings, ", ")) - + // Check if this is a newly created resource - these don't exist yet so missing // attributes are expected, not an error if itemDiff.GetStatus() == sdp.ItemDiffStatus_ITEM_DIFF_STATUS_CREATED { @@ -360,7 +362,7 @@ func mapResourceToQuery(itemDiff *sdp.ItemDiff, terraformResource *Resource, map }, } } - + // For other statuses (REPLACED, UPDATED, DELETED), missing attributes are a real error mappingStatus := sdp.MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_ERROR return PlannedChangeMapResult{ diff --git a/tfutils/plan_mapper_test.go b/tfutils/plan_mapper_test.go index 2e93aa20..e6788c2f 100644 --- a/tfutils/plan_mapper_test.go +++ b/tfutils/plan_mapper_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/xiam/dig"