diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index c96d94e0..4b829f52 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.28.0" + version = "6.30.0" constraints = ">= 4.56.0" hashes = [ - "h1:0aLYWoSJohXeijeD/nB/8wh++Ge2nI/BfsxdZZJf8LI=", - "h1:2bDndcCvti7hgXw4MkMo37cyAAu1gk+JvsU9/UbRJNQ=", - "h1:4xwnb3NQWg9A3SGIYjvhd5WmFbBP++9fLhvcokAKzKc=", - "h1:HYhZgZXKnPKQfV8EFrlKXi4M9YW/RVZ30PpJPKt6Tes=", - "h1:RwoFuX1yGMVaKJaUmXDKklEaQ/yUCEdt5k2kz+/g08c=", - "h1:XOuWba4Jt7rm+TLlLUhIp8m9i5jbA1K9Ab1FUL1PEjE=", - "h1:aGqTvbGcexw1rDeneRME44suXtkM6QKHXEmfEiM8gjc=", - "h1:bMiTeecRXGBmx/btqJX57X0KuJ3j9BmM/ph3KZ3FGj0=", - "h1:dVGoEW/F1Xb7k2bm+O3IG2eCoe/Dxid4p81yy3lbwqs=", - "h1:pAg//+BvaDrvVtw5xgijtsKXAYQGLn2mKl0LBUf6aO8=", - "h1:tsoMkB0xujvKt6jQuRlOGVI++eLTLCYqnVAOAeiNoQQ=", - "h1:we7AZ0oyoO4dyZtYSoAE5nbbGyDrwyc9+99LbnCrb7I=", - "h1:wzZdGs0FFmNqIgPyo9tKnGKJ37BGNSgwRrEXayL29+0=", - "h1:xBJIpfIyvY8aCtkbZM/Om23s7bfNqkpe4pTnBlLiAws=", - "zh:0ba0d5eb6e0c6a933eb2befe3cdbf22b58fbc0337bf138f95bf0e8bb6e6df93e", - "zh:23eacdd4e6db32cf0ff2ce189461bdbb62e46513978d33c5de4decc4670870ec", - "zh:307b06a15fc00a8e6fd243abde2cbe5112e9d40371542665b91bec1018dd6e3c", - "zh:37a02d5b45a9d050b9642c9e2e268297254192280df72f6e46641daca52e40ec", - "zh:3da866639f07d92e734557d673092719c33ede80f4276c835bf7f231a669aa33", - "zh:480060b0ba310d0f6b6a14d60b276698cb103c48fd2f7e2802ae47c963995ec6", - "zh:57796453455c20db80d9168edbf125bf6180e1aae869de1546a2be58e4e405ec", - "zh:69139cba772d4df8de87598d8d8a2b1b4b254866db046c061dccc79edb14e6b9", - "zh:7312763259b859ff911c5452ca8bdf7d0be6231c5ea0de2df8f09d51770900ac", - "zh:8d2d6f4015d3c155d7eb53e36f019a729aefb46ebfe13f3a637327d3a1402ecc", - "zh:94ce589275c77308e6253f607de96919b840c2dd36c44aa798f693c9dd81af42", + "h1:61K3makVG+zqd7eePXPsAFpQZN33Z28Kf9g+OLv/JYM=", + "h1:Bao0MYQHdNQzavGviQLUdR3A1u2NOv6OIc2V5I+VyuY=", + "h1:FNkicntiPhllPhKf8uBJTCQVY/cqN/sXa/LwE4Q0ML8=", + "h1:NHCJ8SQ71K+p5YKety3SY4PvZ0MfIv92c8blfVf4QP0=", + "h1:NNwip4EfMdYRY+fLkSQHekZS67EFeKJmr5Lmu80ajlI=", + "h1:XPXgScQHM4sfRlz7jRNek2/2hj2ZyLhZpaFVtLrzT7o=", + "h1:bpwk17AlZ00qP8tsBna6RUh4qngv+P3VQY26eNtDO4s=", + "h1:iT9e39SGzBWyq7gcmNkBZrWA6vMGJoYMS4CCKHIclqA=", + "h1:ilAhYTs7SG2u59KjKmbIwZq+DAcV7s3cirbjJAMX+ZM=", + "h1:kmLcNCYh5eYzvS+RWD0dxf0qk1u1Ix/8fNkVhSXEciE=", + "h1:qApi394T9DCGHdUsB1JMzZD06uUjyDBI4XYAjBafOY4=", + "h1:rDgnw9NMNfSRUQiv0YZEGWDBqk46RvlBNJt4zOIijMY=", + "h1:u3SrPueECINoUjAy9ix3MG0SaXcfUSghQCMOnnk0FCM=", + "h1:weYTFOITWwcJ7d3/FWWElAYhWcDfyUI19WTct4fdOmg=", + "zh:08fdcbb84b63739b758fd2f657303f495859ae15f2d6c3dbd642520cadb5f063", + "zh:1e69ff49906541cd511bdabcd4b2996a731b1642ba26b834cdac5432e8d5c557", + "zh:3aa23e3af1fb1dd0c025cb8fb73abdabd3f44b6a687a2a239947e7b0201b2f1f", + "zh:4b3b81e63eee913c874e8115d6a83d12bd9d7903446f91be15ba50c583c79549", + "zh:6e93a72d8770d73a4122dc82af33a020d58feeaca4e194a2685dce30dbcdce24", + "zh:74be722c9a64b95e06554cde0bef624084cc5a5ea7f3373f1975b7a4737d7074", + "zh:7d2acf6bc93be26504fd0e2965c77699a49549f74a767d0a81430d9e12d51358", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:adaceec6a1bf4f5df1e12bd72cf52b72087c72efed078aef636f8988325b1a8b", - "zh:d37be1ce187d94fd9df7b13a717c219964cd835c946243f096c6b230cdfd7e92", - "zh:fe6205b5ca2ff36e68395cb8d3ae10a3728f405cdbcd46b206a515e1ebcf17a1", + "zh:aef629bc537b4cc0f64ece87bc2bfdb3e032a4d03a3f7f301f4c84ffdc2ac1ac", + "zh:b41dcc4a2c8e356d82d3f92629aab0e25849db106a43e7adf06d8c6bda7af4c9", + "zh:b4d7a9cf9ad5ac5dd07f4ea1e834b63f14e752f9aca9452cd99570fed16e0c12", + "zh:bcb20f64b9b4599fa746305bcff7eeee3da85029dc467f812f950cf45b519436", + "zh:e45a520b82a1d2d42360db1b93d8e96406a7548948ed528bac5018e1d731c5c6", + "zh:f743e4a0e10dc64669469e6a22e47012f07fb94587f5a1e8cf5431da4e878ae1", + "zh:fe1895af7dcc5815896f892b2593fe71b7f4f364b71d9487d6e8b10ef244c11c", ] } diff --git a/Dockerfile b/Dockerfile index 8ca18a56..89dbb9e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM ghcr.io/opentofu/opentofu:minimal AS tofu -FROM alpine:3.23.2 +FROM alpine:3.23.3 # Copy the tofu binary from the minimal image COPY --from=tofu /usr/local/bin/tofu /usr/local/bin/tofu diff --git a/auth/auth.go b/auth/auth.go index 4ad2000a..6e1bd323 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -123,6 +123,36 @@ func (flowConfig ClientCredentialsConfig) TokenSource(ctx context.Context, oAuth return conf.TokenSource(ctx) } +// Auth0Config contains credentials for creating impersonation HTTP clients +// using Auth0's client credentials flow with account impersonation. +type Auth0Config struct { + Domain string + ClientID string + ClientSecret string + Audience string +} + +// ImpersonationHTTPClient creates an HTTP client that can impersonate the specified account. +// If the config is nil or ClientID is empty, returns a basic tracing HTTP client. +func (c *Auth0Config) ImpersonationHTTPClient(ctx context.Context, accountName string) *http.Client { + if c == nil || c.ClientID == "" { + return tracing.HTTPClient() + } + creds := ClientCredentialsConfig{ + ClientID: c.ClientID, + ClientSecret: c.ClientSecret, + } + ts := creds.TokenSource( + ctx, + fmt.Sprintf("https://%s/oauth/token", c.Domain), + c.Audience, + WithImpersonateAccount(accountName), + ) + // inject otel into oauth2 + ctx = context.WithValue(ctx, oauth2.HTTPClient, tracing.HTTPClient()) + return oauth2.NewClient(ctx, ts) +} + // natsTokenClient A client that is capable of getting NATS JWTs and signing the // required nonce to prove ownership of the NKeys. Satisfies the `TokenClient` // interface @@ -161,7 +191,6 @@ func (n *natsTokenClient) generateJWT(ctx context.Context) error { // If we don't yet have keys generate them if n.keys == nil { err := n.generateKeys() - if err != nil { return err } @@ -257,7 +286,6 @@ func (n *natsTokenClient) GetJWT() (string, error) { func (n *natsTokenClient) Sign(in []byte) ([]byte, error) { if n.keys == nil { err := n.generateKeys() - if err != nil { return []byte{}, err } @@ -303,7 +331,6 @@ func (ats *APIKeyTokenSource) Token() (*oauth2.Token, error) { res, err := ats.apiKeyClient.ExchangeKeyForToken(context.Background(), connect.NewRequest(&sdp.ExchangeKeyForTokenRequest{ ApiKey: ats.ApiKey, })) - if err != nil { return nil, fmt.Errorf("error exchanging API key: %w", err) } @@ -314,7 +341,6 @@ func (ats *APIKeyTokenSource) Token() (*oauth2.Token, error) { // Parse the expiry out of the token token, err := josejwt.ParseSigned(res.Msg.GetAccessToken(), []jose.SignatureAlgorithm{jose.RS256}) - if err != nil { return nil, fmt.Errorf("error parsing JWT: %w", err) } @@ -322,7 +348,6 @@ func (ats *APIKeyTokenSource) Token() (*oauth2.Token, error) { claims := josejwt.Claims{} err = token.UnsafeClaimsWithoutVerification(&claims) - if err != nil { return nil, fmt.Errorf("error parsing JWT claims: %w", err) } diff --git a/auth/middleware.go b/auth/middleware.go index 5a71252e..5a806c55 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -41,8 +41,8 @@ type UserTokenContextKey struct{} // This will be the auth0 `user_id` from the tokens `sub` claim. type CurrentSubjectContextKey struct{} -// AuthConfig Configuration for the auth middleware -type AuthConfig struct { +// MiddlewareConfig Configuration for the auth middleware +type MiddlewareConfig struct { Auth0Domain string Auth0Audience string // The names of the cookies that will be used to authenticate, these will be @@ -169,7 +169,7 @@ func ExtractAccount(ctx context.Context) (string, error) { // therefore the following environment variables must be set: AUTH0_DOMAIN, // AUTH0_AUDIENCE. If cookie auth is intended to be used, then AUTH_COOKIE_NAME // must also be set. -func NewAuthMiddleware(config AuthConfig, next http.Handler) http.Handler { +func NewAuthMiddleware(config MiddlewareConfig, next http.Handler) http.Handler { processOverrides := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { options := []OverrideAuthOptionFunc{} @@ -281,7 +281,7 @@ func withCustomClaims(modify func(*CustomClaims)) OverrideAuthOptionFunc { // // This middleware also extract custom claims form the token and stores them in // CustomClaimsContextKey -func ensureValidTokenHandler(config AuthConfig, next http.Handler) http.Handler { +func ensureValidTokenHandler(config MiddlewareConfig, next http.Handler) http.Handler { if config.Auth0Domain == "" && config.IssuerURL == "" && config.Auth0Audience == "" { log.Fatalf("Auth0 configuration is missing") } diff --git a/auth/middleware_test.go b/auth/middleware_test.go index c17bf685..0e18fd26 100644 --- a/auth/middleware_test.go +++ b/auth/middleware_test.go @@ -127,12 +127,12 @@ func TestNewAuthMiddleware(t *testing.T) { jwksURL := server.Start(ctx) - defaultConfig := AuthConfig{ + defaultConfig := MiddlewareConfig{ IssuerURL: jwksURL, Auth0Audience: "https://api.overmind.tech", } - bypassHealthConfig := AuthConfig{ + bypassHealthConfig := MiddlewareConfig{ IssuerURL: jwksURL, Auth0Audience: "https://api.overmind.tech", BypassAuthForPaths: regexp.MustCompile("/health"), @@ -145,7 +145,7 @@ func TestNewAuthMiddleware(t *testing.T) { Name string TokenOptions *TestTokenOptions ExpectedCode int - AuthConfig AuthConfig + AuthConfig MiddlewareConfig Path string }{ { @@ -263,7 +263,7 @@ func TestNewAuthMiddleware(t *testing.T) { Scope: "test:pass", }, }, - AuthConfig: AuthConfig{ + AuthConfig: MiddlewareConfig{ IssuerURL: jwksURL, Auth0Audience: "https://api.overmind.tech", BypassAuthForPaths: regexp.MustCompile("/health"), @@ -322,7 +322,7 @@ func TestNewAuthMiddleware(t *testing.T) { }, }, ExpectedCode: http.StatusOK, - AuthConfig: AuthConfig{ + AuthConfig: MiddlewareConfig{ IssuerURL: jwksURL, Auth0Audience: "https://api.overmind.tech", BypassAuth: true, @@ -340,7 +340,7 @@ func TestNewAuthMiddleware(t *testing.T) { }, }, ExpectedCode: http.StatusOK, - AuthConfig: AuthConfig{ + AuthConfig: MiddlewareConfig{ IssuerURL: jwksURL, Auth0Audience: "https://api.overmind.tech", BypassAuth: true, @@ -358,7 +358,7 @@ func TestNewAuthMiddleware(t *testing.T) { }, }, ExpectedCode: http.StatusOK, - AuthConfig: AuthConfig{ + AuthConfig: MiddlewareConfig{ IssuerURL: jwksURL, Auth0Audience: "https://api.overmind.tech", AccountOverride: &correctAccount, @@ -376,7 +376,7 @@ func TestNewAuthMiddleware(t *testing.T) { }, }, ExpectedCode: http.StatusOK, - AuthConfig: AuthConfig{ + AuthConfig: MiddlewareConfig{ IssuerURL: jwksURL, Auth0Audience: "https://api.overmind.tech", ScopeOverride: &correctScope, @@ -536,7 +536,7 @@ func TestOverrideAuth(t *testing.T) { } func BenchmarkAuthMiddleware(b *testing.B) { - config := AuthConfig{ + config := MiddlewareConfig{ Auth0Domain: "auth.overmind-demo.com", Auth0Audience: "https://api.overmind.tech", } @@ -705,7 +705,7 @@ func TestConnectErrorHandling(t *testing.T) { jwksURL := server.Start(ctx) // Create the middleware - handler := NewAuthMiddleware(AuthConfig{ + handler := NewAuthMiddleware(MiddlewareConfig{ Auth0Domain: "", Auth0Audience: "test", IssuerURL: jwksURL, diff --git a/aws-source/cmd/root.go b/aws-source/cmd/root.go index 064bd2ee..35252367 100644 --- a/aws-source/cmd/root.go +++ b/aws-source/cmd/root.go @@ -3,12 +3,10 @@ package cmd import ( "context" "fmt" - "net/http" "os" "os/signal" "strings" "syscall" - "time" "github.com/getsentry/sentry-go" "github.com/overmindtech/cli/aws-source/proc" @@ -89,33 +87,7 @@ var rootCmd = &cobra.Command{ log.WithError(err).Fatal("Could not initialize AWS source") } - // Start HTTP server for health checks - // Liveness: Check only engine initialization (NATS, heartbeats) - http.HandleFunc("/healthz/alive", e.LivenessProbeHandlerFunc()) - // Readiness: Check if adapters are healthy and ready to handle requests - http.HandleFunc("/healthz/ready", e.ReadinessProbeHandlerFunc()) - // Backward compatibility - maps to liveness check (matches old behavior) - http.HandleFunc("/healthz", e.LivenessProbeHandlerFunc()) - - log.WithFields(log.Fields{ - "port": healthCheckPort, - }).Debug("Starting healthcheck server with endpoints: /healthz/alive, /healthz/ready, /healthz") - - go func() { - defer sentry.Recover() - - server := &http.Server{ - Addr: fmt.Sprintf(":%v", healthCheckPort), - Handler: nil, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - err := server.ListenAndServe() - - log.WithError(err).WithFields(log.Fields{ - "port": healthCheckPort, - }).Error("Could not start HTTP server for health checks") - }() + e.ServeHealthProbes(healthCheckPort) err = e.Start(ctx) if err != nil { diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index 07d9b368..1af78f20 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -90,30 +90,30 @@ func tryLoadText(ctx context.Context, fileName string) string { return strings.TrimSpace(string(bytes)) } -func createBlastRadiusConfig(maxDepth, maxItems int32, maxTime, changeAnalysisMaxTimeout time.Duration) (*sdp.BlastRadiusConfig, error) { +func createBlastRadiusConfig(maxDepth, maxItems int32, maxTime, changeAnalysisTargetDuration time.Duration) (*sdp.BlastRadiusConfig, error) { var blastRadiusConfigOverride *sdp.BlastRadiusConfig - if maxDepth > 0 || maxItems > 0 || maxTime > 0 || changeAnalysisMaxTimeout > 0 { + if maxDepth > 0 || maxItems > 0 || maxTime > 0 || changeAnalysisTargetDuration > 0 { blastRadiusConfigOverride = &sdp.BlastRadiusConfig{ MaxItems: maxItems, LinkDepth: maxDepth, } // this is for backward compatibility, remove in a future release if maxTime > 0 { - // we convert the maxTime to changeAnalysisMaxTimeout, this means multiplying the (blast radius calculation timeout) maxTime by 1.5 - // eg 10 minute max (blast radius calculation) -> 15 minute changeanalysis max timeout - blastRadiusConfigOverride.ChangeAnalysisMaxTimeout = durationpb.New(time.Duration(float64(maxTime) * 1.5)) + // we convert the maxTime to changeAnalysisTargetDuration, this means multiplying the (blast radius calculation timeout) maxTime by 1.5 + // eg 10 minute max (blast radius calculation) -> 15 minute target duration + blastRadiusConfigOverride.ChangeAnalysisTargetDuration = durationpb.New(time.Duration(float64(maxTime) * 1.5)) } - // Add changeAnalysisMaxTimeout if specified - if changeAnalysisMaxTimeout > 0 { - blastRadiusConfigOverride.ChangeAnalysisMaxTimeout = durationpb.New(changeAnalysisMaxTimeout) + // Add changeAnalysisTargetDuration if specified + if changeAnalysisTargetDuration > 0 { + blastRadiusConfigOverride.ChangeAnalysisTargetDuration = durationpb.New(changeAnalysisTargetDuration) } } - // validate the ChangeAnalysisMaxTimeout - if blastRadiusConfigOverride != nil && blastRadiusConfigOverride.GetChangeAnalysisMaxTimeout() != nil { - changeAnalysisMaxTimeout = blastRadiusConfigOverride.GetChangeAnalysisMaxTimeout().AsDuration() - if changeAnalysisMaxTimeout < 1*time.Minute || changeAnalysisMaxTimeout > 30*time.Minute { - return nil, flagError{"--change-analysis-max-timeout must be between 1 minute and 30 minutes"} + // validate the ChangeAnalysisTargetDuration + if blastRadiusConfigOverride != nil && blastRadiusConfigOverride.GetChangeAnalysisTargetDuration() != nil { + changeAnalysisTargetDuration = blastRadiusConfigOverride.GetChangeAnalysisTargetDuration().AsDuration() + if changeAnalysisTargetDuration < 1*time.Minute || changeAnalysisTargetDuration > 30*time.Minute { + return nil, flagError{"--change-analysis-target-duration must be between 1 minute and 30 minutes"} } } @@ -261,9 +261,9 @@ func SubmitPlan(cmd *cobra.Command, args []string) error { maxDepth := viper.GetInt32("blast-radius-link-depth") maxItems := viper.GetInt32("blast-radius-max-items") maxTime := viper.GetDuration("blast-radius-max-time") - changeAnalysisMaxTimeout := viper.GetDuration("change-analysis-max-timeout") + changeAnalysisTargetDuration := viper.GetDuration("change-analysis-target-duration") - blastRadiusConfigOverride, err := createBlastRadiusConfig(maxDepth, maxItems, maxTime, changeAnalysisMaxTimeout) + blastRadiusConfigOverride, err := createBlastRadiusConfig(maxDepth, maxItems, maxTime, changeAnalysisTargetDuration) if err != nil { return err } @@ -394,8 +394,8 @@ func init() { submitPlanCmd.PersistentFlags().Int32("blast-radius-max-items", 0, "Used in combination with '--blast-radius-link-depth' to customise how many items are included in the blast radius. Larger numbers will result in a more comprehensive blast radius, but may take longer to calculate. Defaults to the account level settings.") submitPlanCmd.PersistentFlags().Duration("blast-radius-max-time", 0, "Maximum time duration for blast radius calculation (e.g., '5m', '15m', '30m'). When the time limit is reached, the analysis continues with risks identified up to that point. Defaults to the account level settings (QUICK: 10m, DETAILED: 15m, FULL: 30m). Valid range: 1m to 30m.") - _ = submitPlanCmd.PersistentFlags().MarkDeprecated("blast-radius-max-time", "This flag is no longer used and will be removed in a future release. Use the '--change-analysis-max-timeout' flag instead.") - submitPlanCmd.PersistentFlags().Duration("change-analysis-max-timeout", 0, "Maximum time duration for change analysis (e.g., '5m', '15m', '30m'). When the time limit is reached, the analysis continues with risks identified up to that point. Defaults to the account level settings (QUICK: 10m, DETAILED: 15m, FULL: 30m). Valid range: 1m to 30m.") + _ = submitPlanCmd.PersistentFlags().MarkDeprecated("blast-radius-max-time", "This flag is no longer used and will be removed in a future release. Use the '--change-analysis-target-duration' flag instead.") + submitPlanCmd.PersistentFlags().Duration("change-analysis-target-duration", 0, "Target duration for change analysis planning (e.g., '5m', '15m', '30m'). This is NOT a hard deadline - the blast radius phase uses 67% of this target to stop gracefully. The job can run slightly past this target and is only hard-stopped at 30 minutes. Defaults to the account level settings (QUICK: 10m, DETAILED: 15m, FULL: 30m). Valid range: 1m to 30m.") submitPlanCmd.PersistentFlags().String("auto-tag-rules", "", "The path to the auto-tag rules file. If not provided, it will check the default location which is '.overmind/auto-tag-rules.yaml'. If no rules are found locally, the rules configured through the UI are used.") submitPlanCmd.PersistentFlags().String("signal-config", "", "The path to the signal config file. If not provided, it will check the default location which is '.overmind/signal-config.yaml'. If no config is found locally, the config configured through the UI is used.") } diff --git a/cmd/changes_submit_plan_test.go b/cmd/changes_submit_plan_test.go index 32a056bf..b97b3d59 100644 --- a/cmd/changes_submit_plan_test.go +++ b/cmd/changes_submit_plan_test.go @@ -13,12 +13,12 @@ func TestBlastRadiusConfigCreation(t *testing.T) { blastRadiusMaxDepth int32 blastRadiusMaxItems int32 blastRadiusMaxTime time.Duration - changeAnalysisMaxTimeout time.Duration + changeAnalysisTargetDuration time.Duration expectBlastRadiusConfig bool expectedBlastRadiusMaxItems int32 expectedBlastRadiusLinkDepth int32 - expectChangeAnalysisMaxTimeout bool - expectedChangeAnalysisMaxTimeout time.Duration + expectChangeAnalysisTargetDuration bool + expectedChangeAnalysisTargetDuration time.Duration expectError bool expectedErrorMsg string }{ @@ -59,20 +59,20 @@ func TestBlastRadiusConfigCreation(t *testing.T) { // The server should treat 0 values as "use defaults" rather than literal zeros. expectedBlastRadiusMaxItems: 0, expectedBlastRadiusLinkDepth: 0, - expectChangeAnalysisMaxTimeout: true, - expectedChangeAnalysisMaxTimeout: 15 * time.Minute, // maxTime * 1.5 + expectChangeAnalysisTargetDuration: true, + expectedChangeAnalysisTargetDuration: 15 * time.Minute, // maxTime * 1.5 }, { name: "All flags specified", blastRadiusMaxDepth: 5, blastRadiusMaxItems: 1000, blastRadiusMaxTime: 15 * time.Minute, - changeAnalysisMaxTimeout: 20 * time.Minute, + changeAnalysisTargetDuration: 20 * time.Minute, expectBlastRadiusConfig: true, expectedBlastRadiusMaxItems: 1000, expectedBlastRadiusLinkDepth: 5, - expectChangeAnalysisMaxTimeout: true, - expectedChangeAnalysisMaxTimeout: 20 * time.Minute, // changeAnalysisMaxTimeout overrides maxTime + expectChangeAnalysisTargetDuration: true, + expectedChangeAnalysisTargetDuration: 20 * time.Minute, // changeAnalysisTargetDuration overrides maxTime }, { name: "maxTime and maxDepth specified", @@ -82,8 +82,8 @@ func TestBlastRadiusConfigCreation(t *testing.T) { expectBlastRadiusConfig: true, expectedBlastRadiusMaxItems: 0, expectedBlastRadiusLinkDepth: 3, - expectChangeAnalysisMaxTimeout: true, - expectedChangeAnalysisMaxTimeout: 7*time.Minute + 30*time.Second, // maxTime * 1.5 + expectChangeAnalysisTargetDuration: true, + expectedChangeAnalysisTargetDuration: 7*time.Minute + 30*time.Second, // maxTime * 1.5 }, { name: "maxTime and maxItems specified", @@ -93,40 +93,40 @@ func TestBlastRadiusConfigCreation(t *testing.T) { expectBlastRadiusConfig: true, expectedBlastRadiusMaxItems: 500, expectedBlastRadiusLinkDepth: 0, - expectChangeAnalysisMaxTimeout: true, - expectedChangeAnalysisMaxTimeout: 30 * time.Minute, // maxTime * 1.5 + expectChangeAnalysisTargetDuration: true, + expectedChangeAnalysisTargetDuration: 30 * time.Minute, // maxTime * 1.5 }, { - name: "Only changeAnalysisMaxTimeout specified", + name: "Only changeAnalysisTargetDuration specified", blastRadiusMaxDepth: 0, blastRadiusMaxItems: 0, blastRadiusMaxTime: 0, - changeAnalysisMaxTimeout: 10 * time.Minute, + changeAnalysisTargetDuration: 10 * time.Minute, expectBlastRadiusConfig: true, expectedBlastRadiusMaxItems: 0, expectedBlastRadiusLinkDepth: 0, - expectChangeAnalysisMaxTimeout: true, - expectedChangeAnalysisMaxTimeout: 10 * time.Minute, + expectChangeAnalysisTargetDuration: true, + expectedChangeAnalysisTargetDuration: 10 * time.Minute, }, { - name: "changeAnalysisMaxTimeout too low", + name: "changeAnalysisTargetDuration too low", blastRadiusMaxDepth: 0, blastRadiusMaxItems: 0, blastRadiusMaxTime: 0, - changeAnalysisMaxTimeout: 30 * time.Second, + changeAnalysisTargetDuration: 30 * time.Second, expectBlastRadiusConfig: true, expectError: true, - expectedErrorMsg: "--change-analysis-max-timeout must be between 1 minute and 30 minutes", + expectedErrorMsg: "--change-analysis-target-duration must be between 1 minute and 30 minutes", }, { - name: "changeAnalysisMaxTimeout too high", + name: "changeAnalysisTargetDuration too high", blastRadiusMaxDepth: 0, blastRadiusMaxItems: 0, blastRadiusMaxTime: 0, - changeAnalysisMaxTimeout: 31 * time.Minute, + changeAnalysisTargetDuration: 31 * time.Minute, expectBlastRadiusConfig: true, expectError: true, - expectedErrorMsg: "--change-analysis-max-timeout must be between 1 minute and 30 minutes", + expectedErrorMsg: "--change-analysis-target-duration must be between 1 minute and 30 minutes", }, { name: "maxTime results in timeout too low", @@ -135,7 +135,7 @@ func TestBlastRadiusConfigCreation(t *testing.T) { blastRadiusMaxTime: 30 * time.Second, // * 1.5 = 45 seconds, which is < 1 minute expectBlastRadiusConfig: true, expectError: true, - expectedErrorMsg: "--change-analysis-max-timeout must be between 1 minute and 30 minutes", + expectedErrorMsg: "--change-analysis-target-duration must be between 1 minute and 30 minutes", }, { name: "maxTime results in timeout too high", @@ -144,7 +144,7 @@ func TestBlastRadiusConfigCreation(t *testing.T) { blastRadiusMaxTime: 21 * time.Minute, // * 1.5 = 31.5 minutes, which is > 30 minutes expectBlastRadiusConfig: true, expectError: true, - expectedErrorMsg: "--change-analysis-max-timeout must be between 1 minute and 30 minutes", + expectedErrorMsg: "--change-analysis-target-duration must be between 1 minute and 30 minutes", }, } @@ -152,7 +152,7 @@ func TestBlastRadiusConfigCreation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - blastRadiusConfigOverride, err := createBlastRadiusConfig(tt.blastRadiusMaxDepth, tt.blastRadiusMaxItems, tt.blastRadiusMaxTime, tt.changeAnalysisMaxTimeout) + blastRadiusConfigOverride, err := createBlastRadiusConfig(tt.blastRadiusMaxDepth, tt.blastRadiusMaxItems, tt.blastRadiusMaxTime, tt.changeAnalysisTargetDuration) // Check error expectations if tt.expectError { @@ -188,18 +188,18 @@ func TestBlastRadiusConfigCreation(t *testing.T) { if blastRadiusConfigOverride.GetLinkDepth() != tt.expectedBlastRadiusLinkDepth { t.Errorf("Expected LinkDepth to be %d, but got %d", tt.expectedBlastRadiusLinkDepth, blastRadiusConfigOverride.GetLinkDepth()) } - if tt.expectChangeAnalysisMaxTimeout { - if blastRadiusConfigOverride.GetChangeAnalysisMaxTimeout() == nil { - t.Errorf("Expected ChangeAnalysisMaxTimeout to be set, but got nil") + if tt.expectChangeAnalysisTargetDuration { + if blastRadiusConfigOverride.GetChangeAnalysisTargetDuration() == nil { + t.Errorf("Expected ChangeAnalysisTargetDuration to be set, but got nil") } else { - actualTimeout := blastRadiusConfigOverride.GetChangeAnalysisMaxTimeout().AsDuration() - if actualTimeout != tt.expectedChangeAnalysisMaxTimeout { - t.Errorf("Expected ChangeAnalysisMaxTimeout to be %v, but got %v", tt.expectedChangeAnalysisMaxTimeout, actualTimeout) + actualTimeout := blastRadiusConfigOverride.GetChangeAnalysisTargetDuration().AsDuration() + if actualTimeout != tt.expectedChangeAnalysisTargetDuration { + t.Errorf("Expected ChangeAnalysisTargetDuration to be %v, but got %v", tt.expectedChangeAnalysisTargetDuration, actualTimeout) } } } else { - if blastRadiusConfigOverride.GetChangeAnalysisMaxTimeout() != nil { - t.Errorf("Expected ChangeAnalysisMaxTimeout to be nil, but got %v", blastRadiusConfigOverride.GetChangeAnalysisMaxTimeout()) + if blastRadiusConfigOverride.GetChangeAnalysisTargetDuration() != nil { + t.Errorf("Expected ChangeAnalysisTargetDuration to be nil, but got %v", blastRadiusConfigOverride.GetChangeAnalysisTargetDuration()) } } } diff --git a/discovery/adapter_test.go b/discovery/adapter_test.go index d5dcf71f..4b427c1c 100644 --- a/discovery/adapter_test.go +++ b/discovery/adapter_test.go @@ -36,7 +36,7 @@ func TestGet(t *testing.T) { "test", "empty", }, - cacheField: sdpcache.NewCache(t.Context()), + cache: sdpcache.NewCache(t.Context()), } e := newStartedEngine(t, "TestGet", nil, nil, &adapter) @@ -307,7 +307,7 @@ func TestListSearchCaching(t *testing.T) { "empty", "error", }, - cacheField: sdpcache.NewCache(t.Context()), + cache: sdpcache.NewCache(t.Context()), } e := newStartedEngine(t, "TestListSearchCaching", nil, nil, &adapter) @@ -619,7 +619,7 @@ func TestSearchGetCaching(t *testing.T) { ReturnScopes: []string{ "test", }, - cacheField: sdpcache.NewCache(t.Context()), + cache: sdpcache.NewCache(t.Context()), } e := newStartedEngine(t, "TestSearchGetCaching", nil, nil, &adapter) diff --git a/discovery/engine.go b/discovery/engine.go index 4cf83284..61fa08d0 100644 --- a/discovery/engine.go +++ b/discovery/engine.go @@ -768,3 +768,32 @@ func (e *Engine) ReadinessProbeHandlerFunc() func(http.ResponseWriter, *http.Req fmt.Fprint(rw, "ok") } } + +// ServeHealthProbes starts an HTTP server for Kubernetes health probes on the given port. +// Registers /healthz/alive (liveness), /healthz/ready (readiness), and /healthz (backward compat). +// 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") + + go func() { + defer sentry.Recover() + server := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + err := server.ListenAndServe() + log.WithError(err).WithFields(logFields).Error("Could not start HTTP server for health checks") + }() +} diff --git a/discovery/enginerequests.go b/discovery/enginerequests.go index 9f70c595..c3bf6856 100644 --- a/discovery/enginerequests.go +++ b/discovery/enginerequests.go @@ -94,20 +94,22 @@ func (e *Engine) HandleQuery(ctx context.Context, query *sdp.Query) { // Only start the span if we actually have something that will respond ctx, span := tracer.Start(ctx, "HandleQuery", trace.WithAttributes( attribute.Int("ovm.discovery.numExpandedQueries", numExpandedQueries), - attribute.String("ovm.sdp.uuid", u.String()), - attribute.String("ovm.sdp.type", query.GetType()), - attribute.String("ovm.sdp.method", query.GetMethod().String()), - attribute.String("ovm.sdp.query", query.GetQuery()), - attribute.String("ovm.sdp.scope", query.GetScope()), - attribute.String("ovm.sdp.deadline", query.GetDeadline().AsTime().String()), attribute.Bool("ovm.sdp.deadlineOverridden", deadlineOverride), - attribute.Bool("ovm.sdp.queryIgnoreCache", query.GetIgnoreCache()), attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), attribute.String("ovm.engine.type", e.EngineConfig.EngineType), attribute.String("ovm.engine.version", e.EngineConfig.Version), )) defer span.End() + query.SetSpanAttributes(span) + + deadline, ok := ctx.Deadline() + if ok { + span.SetAttributes( + attribute.String("ovm.sdp.ctxDeadline", deadline.String()), + ) + } + if query.GetRecursionBehaviour() != nil { span.SetAttributes( attribute.Int("ovm.sdp.linkDepth", int(query.GetRecursionBehaviour().GetLinkDepth())), @@ -294,11 +296,11 @@ func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses c // could indicate a bug in the adapter. Make sure to // keep the trigger and this message in sync. log.WithContext(ctx).WithFields(log.Fields{ - "ovm.query.uuid": q.GetUUIDParsed().String(), - "ovm.query.type": q.GetType(), - "ovm.query.scope": q.GetScope(), - "ovm.query.method": q.GetMethod().String(), - "ovm.query.adapter": adapter.Name(), + "ovm.sdp.uuid": q.GetUUIDParsed().String(), + "ovm.sdp.type": q.GetType(), + "ovm.sdp.scope": q.GetScope(), + "ovm.sdp.method": q.GetMethod().String(), + "ovm.adapter.name": adapter.Name(), }).Errorf("Wait group still running %v after context cancelled", longRunningAdaptersTimeout) } expandedMutex.RUnlock() @@ -324,17 +326,21 @@ func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses c // called in parallel with other queries and the results should be merged func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, responses chan<- *sdp.QueryResponse) { ctx, span := tracer.Start(ctx, "Execute", trace.WithAttributes( + attribute.String("ovm.adapter.name", adapter.Name()), + attribute.String("ovm.engine.type", e.EngineConfig.EngineType), + attribute.String("ovm.engine.version", e.EngineConfig.Version), + attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), + + // deprecated, we are keeping these here for data integrity of old queries until 2026-03-01 attribute.String("ovm.adapter.queryMethod", q.GetMethod().String()), attribute.String("ovm.adapter.queryType", q.GetType()), attribute.String("ovm.adapter.queryScope", q.GetScope()), - attribute.String("ovm.adapter.name", adapter.Name()), attribute.String("ovm.adapter.query", q.GetQuery()), - attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), - attribute.String("ovm.engine.type", e.EngineConfig.EngineType), - attribute.String("ovm.engine.version", e.EngineConfig.Version), )) defer span.End() + q.SetSpanAttributes(span) + // We want to avoid having a Get and a List running at the same time, we'd // rather run the List first, populate the cache, then have the Get just // grab the value from the cache. To this end we use a GetListMutex to allow @@ -351,11 +357,6 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res // will only ever have a cache hit if the query is identical } - span.SetAttributes( - attribute.String("ovm.adapter.queryType", q.GetType()), - attribute.String("ovm.adapter.queryScope", q.GetScope()), - ) - // Ensure that the span is closed when the context is done. This is based on // the assumption that some adapters may not respect the context deadline and // may run indefinitely. This ensures that we at least get notified about @@ -472,9 +473,9 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res } else { // Log the error instead of sending it over the stream log.WithContext(ctx).WithFields(log.Fields{ - "ovm.adapter.name": adapter.Name(), - "ovm.adapter.type": q.GetType(), - "ovm.adapter.scope": q.GetScope(), + "ovm.adapter.name": adapter.Name(), + "ovm.sdp.type": q.GetType(), + "ovm.sdp.scope": q.GetScope(), }).Warn("adapter is not listable") } case sdp.QueryMethod_SEARCH: @@ -496,9 +497,9 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res } else { // Log the error instead of sending it over the stream log.WithContext(ctx).WithFields(log.Fields{ - "ovm.adapter.name": adapter.Name(), - "ovm.adapter.type": q.GetType(), - "ovm.adapter.scope": q.GetScope(), + "ovm.adapter.name": adapter.Name(), + "ovm.sdp.type": q.GetType(), + "ovm.sdp.scope": q.GetScope(), }).Warn("adapter is not searchable") } } diff --git a/discovery/enginerequests_test.go b/discovery/enginerequests_test.go index 071e21d4..f7593dd7 100644 --- a/discovery/enginerequests_test.go +++ b/discovery/enginerequests_test.go @@ -58,7 +58,9 @@ func TestExecuteQuery(t *testing.T) { ) t.Run("Basic happy-path Get query", func(t *testing.T) { + u := uuid.New() q := &sdp.Query{ + UUID: u[:], Type: "person", Method: sdp.QueryMethod_GET, Query: "foo", @@ -92,6 +94,8 @@ func TestExecuteQuery(t *testing.T) { item := items[0] if !reflect.DeepEqual(item.GetMetadata().GetSourceQuery(), q) { + t.Logf("adapter query: %+v", item.GetMetadata().GetSourceQuery()) + t.Logf("expected query: %+v", q) t.Error("adapter query mismatch") } }) diff --git a/discovery/shared_test.go b/discovery/shared_test.go index feff500a..3bcf4536 100644 --- a/discovery/shared_test.go +++ b/discovery/shared_test.go @@ -77,13 +77,13 @@ type TestAdapter struct { mutex sync.Mutex CacheDuration time.Duration // How long to cache items for - cacheField sdpcache.Cache // The cache for this adapter (set during creation, can be nil for tests) + cache sdpcache.Cache // This is mandatory } // NewTestAdapter creates a new TestAdapter with cache initialized func NewTestAdapter() *TestAdapter { return &TestAdapter{ - cacheField: sdpcache.NewNoOpCache(), // Initialize with NoOpCache to avoid nil pointer dereferences + cache: sdpcache.NewNoOpCache(), // Initialize with NoOpCache to avoid nil pointer dereferences } } @@ -98,8 +98,8 @@ func (s *TestAdapter) ClearCalls() { s.ListCalls = make([][]string, 0) s.SearchCalls = make([][]string, 0) s.GetCalls = make([][]string, 0) - if s.cacheField != nil { - s.cacheField.Clear() + if s.cache != nil { + s.cache.Clear() } } @@ -132,13 +132,13 @@ var ( ) func (s *TestAdapter) Cache() sdpcache.Cache { - if s.cacheField == nil { + if s.cache == nil { noOpCacheTestOnce.Do(func() { noOpCacheTest = sdpcache.NewNoOpCache() }) return noOpCacheTest } - return s.cacheField + return s.cache } func (s *TestAdapter) Scopes() []string { diff --git a/go.mod b/go.mod index 4a92b352..03a4937e 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( cloud.google.com/go/bigtable v1.41.0 cloud.google.com/go/compute v1.54.0 cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/container v1.45.0 + cloud.google.com/go/container v1.46.0 cloud.google.com/go/dataplex v1.28.0 cloud.google.com/go/dataproc/v2 v2.15.0 cloud.google.com/go/filestore v1.10.3 @@ -40,17 +40,17 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch v1.2.1 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2 v2.0.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8 v8.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2 v2.1.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2 v2.0.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.7 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3 v3.0.0 github.com/Masterminds/semver/v3 v3.4.0 github.com/MrAlias/otel-schema-utils v0.4.0-alpha github.com/auth0/go-jwt-middleware/v2 v2.3.1 @@ -64,7 +64,7 @@ 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.54.0 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.281.0 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.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.0 @@ -72,12 +72,12 @@ require ( github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6 github.com/aws/aws-sdk-go-v2/service/iam v1.53.2 github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 - github.com/aws/aws-sdk-go-v2/service/lambda v1.87.1 + 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/route53 v1.62.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.95.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 github.com/aws/aws-sdk-go-v2/service/sqs v1.42.21 github.com/aws/aws-sdk-go-v2/service/ssm v1.67.8 @@ -88,7 +88,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 github.com/coder/websocket v1.8.14 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/getsentry/sentry-go v0.41.0 + github.com/getsentry/sentry-go v0.42.0 github.com/go-jose/go-jose/v4 v4.1.3 github.com/google/btree v1.1.3 github.com/google/uuid v1.6.0 @@ -103,11 +103,11 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/reflow v0.3.0 github.com/nats-io/jwt/v2 v2.8.0 - github.com/nats-io/nats-server/v2 v2.12.3 + github.com/nats-io/nats-server/v2 v2.12.4 github.com/nats-io/nats.go v1.48.0 github.com/nats-io/nkeys v0.4.12 - github.com/onsi/ginkgo/v2 v2.27.5 // indirect - github.com/onsi/gomega v1.39.0 // indirect + github.com/onsi/ginkgo/v2 v2.28.1 // indirect + github.com/onsi/gomega v1.39.1 // indirect github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c @@ -135,12 +135,11 @@ require ( go.uber.org/mock v0.6.0 golang.org/x/net v0.49.0 golang.org/x/oauth2 v0.34.0 - golang.org/x/sync v0.19.0 // indirect + golang.org/x/sync v0.19.0 golang.org/x/text v0.33.0 gonum.org/v1/gonum v0.17.0 - google.golang.org/api v0.262.0 - google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d - google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d + google.golang.org/api v0.264.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.1 @@ -212,8 +211,7 @@ require ( github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-tpm v0.9.7 // indirect - github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/gookit/color v1.5.4 // indirect @@ -224,7 +222,7 @@ require ( 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 - github.com/klauspost/compress v1.18.2 // indirect + github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect @@ -273,12 +271,13 @@ require ( 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/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect + golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect golang.org/x/term v0.39.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.40.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-20260120174246-409b4a993575 // indirect + google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // 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 diff --git a/go.sum b/go.sum index 9aeff098..6abb810c 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ cloud.google.com/go/compute v1.54.0 h1:4CKmnpO+40z44bKG5bdcKxQ7ocNpRtOc9SCLLUzze cloud.google.com/go/compute v1.54.0/go.mod h1:RfBj0L1x/pIM84BrzNX2V21oEv16EKRPBiTcBRRH1Ww= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/container v1.45.0 h1:i1No5obpPxlIFLGHdUF6h2YjRR1qN9t/ZkA8KA5B//o= -cloud.google.com/go/container v1.45.0/go.mod h1:eB6jUfJLjne9VsTDGcH7mnj6JyZK+KOUIA6KZnYE/ds= +cloud.google.com/go/container v1.46.0 h1:xX94Lo3xrS5OkdMWKvpEVAbBwjN9uleVv6vOi02fL4s= +cloud.google.com/go/container v1.46.0/go.mod h1:A7gMqdQduTk46+zssWDTKbGS2z46UsJNXfKqvMI1ZO4= cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k= cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= cloud.google.com/go/dataplex v1.28.0 h1:rROI3iqMVI9nXT701ULoFRETQVAOAPC3mPSWFDxXFl0= @@ -84,8 +84,8 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDo github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 h1:qiir/pptnHqp6hV8QwV+IExYIf6cPsXBfUDUXQ27t2Y= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2/go.mod h1:jVRrRDLCOuif95HDYC23ADTMlvahB7tMdl519m9Iyjc= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch v1.2.1 h1:IdXgoDe3cTMEGXpaW1Y9sLNRhY4iy0Ul2rXGRfMlWLI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch v1.2.1/go.mod h1:CHiiIYxbQfbFdCvAgmJ5/Ivp+s4tz+dQ9nO0Z7InRZY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1 h1:6aObZUybvkz7Sm2d/GxgsZ+0hbhA0RC5p+81aAxQ/Po= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1/go.mod h1:kz6cfDXtcUJWUjLKSlXW+oBqtWovK648UYJDZYtAZ3g= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0 h1:nyxugFxG2uhbMeJVCFFuD2j9wu+6KgeabITdINraQsE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0/go.mod h1:e4RAYykLIz73CF52KhSooo4whZGXvXrD09m0jkgnWiU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos v1.0.0 h1:Fv8iibGn1eSw0lt2V3cTsuokBEnOP+M//n8OiMcCgTM= @@ -94,26 +94,24 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxw github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0 h1:nnQ9vXH039UrEFxi08pPuZBE7VfqSJt343uJLw0rhWI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0/go.mod h1:4YIVtzMFVsPwBvitCDX7J9sqthSj43QD1sP6fYc1egc= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2 v2.0.1 h1:nFZ7AvJqTpWobmnZlprsK6GucrByFsXWB+DwkhRxM9I= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2 v2.0.1/go.mod h1:ZNiswYTEPuQ/D+mHxONII+FeHHNNVQlJ5IUG88opjS0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.2.0 h1:akP6VpxJGgQRpDR1P462piz/8OhYLRCreDj48AyNabc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.2.0/go.mod h1:8wzvopPfyZYPaQUoKW87Zfdul7jmJMDfp/k7YY3oJyA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 h1:L7G3dExHBgUxsO3qpTGhk/P2dgnYyW48yn7AO33Tbek= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0/go.mod h1:Ms6gYEy0+A2knfKrwdatsggTXYA2+ICKug8w7STorFw= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0 h1:HYGD75g0bQ3VO/Omedm54v4LrD3B1cGImuRF3AJ5wLo= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0/go.mod h1:ulHyBFJOI0ONiRL4vcJTmS7rx18jQQlEPmAgo80cRdM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8 v8.0.0 h1:7QO7GhGat25QEYL4h607O9zNNTUlAv8PbSesW6Ol5Gg= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8 v8.0.0/go.mod h1:mCqeYzwyjn/pw0JVqHJMIzfUQJrlcV0YjTg5b0NK+F0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0 h1:S7K+MLPEYe+g9AX9dLKldBpYV03bPl7zeDaWhiNDqqs= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0/go.mod h1:EHRrmrnS2Q8fB3+DE30TTk04JLqjui5ZJEF7eMVQ2/M= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2 v2.1.0 h1:seyVIpxalxYmfjoo8MB4rRzWaobMG+KJ2+MAUrEvDGU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2 v2.1.0/go.mod h1:M3QD7IyKZBaC4uAKjitTOSOXdcPC6JS1A9oOW3hYjbQ= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 h1:S087deZ0kP1RUg4pU7w9U9xpUedTCbOtz+mnd0+hrkQ= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0/go.mod h1:B4cEyXrWBmbfMDAPnpJ1di7MAt5DKP57jPEObAvZChg= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2 v2.0.0 h1:+vh02EiRx2UmL9NDoA36U18Bgwl9luxs6ia0GAI9Rzg= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2 v2.0.0/go.mod h1:iKOtU3WyuNvNc4L1Z4IxHaoO0dGq5tg+uhLix/KRmzE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.7 h1:SLsVdG/8T65poVMw5ZJtI/dUL7iIwvbkq+koqmWdmu8= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.7/go.mod h1:l9kSL5eB+KdZ2aovhkUYwyZE7oQwTEqVCxnpNKChi1U= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3 v3.0.0 h1:tqGq5xt/rNU57Eb52rf6bvrNWoKPSwLDVUQrJnF4C5U= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3 v3.0.0/go.mod h1:HfDdtu9K0iFBSMMxFsHJPkAAxFWd2IUOW8HU8kEdF3Y= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= @@ -192,8 +190,8 @@ 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.54.0 h1:SW3MUVGaqOv/h4spv3IubyGz9CpvE0gHWEJsZQNPFMs= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.54.0/go.mod h1:ctEsEHY2vFQc6i4KU07q4n68v7BAmTbujv2Y+z8+hQY= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.281.0 h1:9bFLf1b1EQS9JWghInM4cLlfv7bfJCdW5I6dECnWens= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.281.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY= +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/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= @@ -218,8 +216,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= -github.com/aws/aws-sdk-go-v2/service/lambda v1.87.1 h1:QBdmTXWwqVgx0PueT/Xgp2+al5HR0gAV743pTzYeBRw= -github.com/aws/aws-sdk-go-v2/service/lambda v1.87.1/go.mod h1:ogjbkxFgFOjG3dYFQ8irC92gQfpfMDcy1RDKNSZWXNU= +github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0 h1:u66DMbJWDFXs9458RAHNtq2d0gyqcZFV4mzRwfjM358= +github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0/go.mod h1:ogjbkxFgFOjG3dYFQ8irC92gQfpfMDcy1RDKNSZWXNU= github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3 h1:Fobn9IdJv8lgpGv5BYR5m3sFwlMctKgKE9rMRKVKpIQ= 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= @@ -228,8 +226,8 @@ github.com/aws/aws-sdk-go-v2/service/rds v1.114.0 h1:p9c6HDzx6sTf7uyc9xsQd693uzA 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/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.95.1 h1:C2dUPSnEpy4voWFIq3JNd8gN0Y5vYGDo44eUE58a/p8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 h1:Ke7RS0NuP9Xwk31prXYcFGA1Qfn8QmNWcxyjKPcXZdc= @@ -309,8 +307,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/getsentry/sentry-go v0.41.0 h1:q/dQZOlEIb4lhxQSjJhQqtRr3vwrJ6Ahe1C9zv+ryRo= -github.com/getsentry/sentry-go v0.41.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= +github.com/getsentry/sentry-go v0.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7RLEt8= +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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= @@ -348,13 +346,13 @@ github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnL github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA= -github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= -github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -401,8 +399,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -459,18 +457,18 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= -github.com/nats-io/nats-server/v2 v2.12.3 h1:KRv+1n7lddMVgkJPQer+pt36TcO0ENxjilBmeWdjcHs= -github.com/nats-io/nats-server/v2 v2.12.3/go.mod h1:MQXjG9WjyXKz9koWzUc3jYUMKD8x3CLmTNy91IQQz3Y= +github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts= +github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc= github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg= 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/onsi/ginkgo/v2 v2.27.5 h1:ZeVgZMx2PDMdJm/+w5fE/OyG6ILo1Y3e+QX4zSR0zTE= -github.com/onsi/ginkgo/v2 v2.27.5/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= -github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +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= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd h1:UuQycBx6K0lB0/IfHePshOYjlrptkF4FoApFP2Y4s3k= github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd/go.mod h1:391Ww1JbjG4FHOlvQqCd6n25CCCPE64JzC5cCYPxhyM= github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297 h1:ih4bqBMHTCtg3lMwJszNkMGO9n7Uoe0WX5be1/x+s+g= @@ -667,8 +665,8 @@ 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/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +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= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -689,21 +687,21 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +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-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.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY= -google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI= -google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d h1:hUplc9kLwH374NIY3PreRUK3Unc0xLm/W7MDsm0gCNo= -google.golang.org/genproto v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:SpjiK7gGN2j/djoQMxLl3QOe/J/XxNzC5M+YLecVVWU= -google.golang.org/genproto/googleapis/api v0.0.0-20260120174246-409b4a993575 h1:FWSX7MpEdo8+769wkFCFqNMjLV8mDyS8EI1nIG4ysCc= -google.golang.org/genproto/googleapis/api v0.0.0-20260120174246-409b4a993575/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/api v0.264.0 h1:+Fo3DQXBK8gLdf8rFZ3uLu39JpOnhvzJrLMQSoSYZJM= +google.golang.org/api v0.264.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8= +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-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4= +google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/k8s-source/adapters/clusterrole.go b/k8s-source/adapters/clusterrole.go index a6f24700..955436d4 100644 --- a/k8s-source/adapters/clusterrole.go +++ b/k8s-source/adapters/clusterrole.go @@ -13,7 +13,7 @@ func newClusterRoleAdapter(cs *kubernetes.Clientset, cluster string, namespaces return &KubeTypeAdapter[*v1.ClusterRole, *v1.ClusterRoleList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "ClusterRole", ClusterInterfaceBuilder: func() ItemInterface[*v1.ClusterRole, *v1.ClusterRoleList] { return cs.RbacV1().ClusterRoles() diff --git a/k8s-source/adapters/clusterrolebinding.go b/k8s-source/adapters/clusterrolebinding.go index bcd65c99..17d479f0 100644 --- a/k8s-source/adapters/clusterrolebinding.go +++ b/k8s-source/adapters/clusterrolebinding.go @@ -78,7 +78,7 @@ func newClusterRoleBindingAdapter(cs *kubernetes.Clientset, cluster string, name return &KubeTypeAdapter[*v1.ClusterRoleBinding, *v1.ClusterRoleBindingList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "ClusterRoleBinding", ClusterInterfaceBuilder: func() ItemInterface[*v1.ClusterRoleBinding, *v1.ClusterRoleBindingList] { return cs.RbacV1().ClusterRoleBindings() diff --git a/k8s-source/adapters/configmap.go b/k8s-source/adapters/configmap.go index ddfcdad8..a25c99cd 100644 --- a/k8s-source/adapters/configmap.go +++ b/k8s-source/adapters/configmap.go @@ -14,7 +14,7 @@ func newConfigMapAdapter(cs *kubernetes.Clientset, cluster string, namespaces [] Namespaces: namespaces, TypeName: "ConfigMap", AutoQueryExtract: true, - cacheField: cache, + cache: cache, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.ConfigMap, *v1.ConfigMapList] { return cs.CoreV1().ConfigMaps(namespace) }, diff --git a/k8s-source/adapters/cronjob.go b/k8s-source/adapters/cronjob.go index b2f89a2e..7180f115 100644 --- a/k8s-source/adapters/cronjob.go +++ b/k8s-source/adapters/cronjob.go @@ -13,7 +13,7 @@ func newCronJobAdapter(cs *kubernetes.Clientset, cluster string, namespaces []st return &KubeTypeAdapter[*v1.CronJob, *v1.CronJobList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "CronJob", AutoQueryExtract: true, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.CronJob, *v1.CronJobList] { diff --git a/k8s-source/adapters/daemonset.go b/k8s-source/adapters/daemonset.go index 89d7b00c..091b62bd 100644 --- a/k8s-source/adapters/daemonset.go +++ b/k8s-source/adapters/daemonset.go @@ -15,7 +15,7 @@ func newDaemonSetAdapter(cs *kubernetes.Clientset, cluster string, namespaces [] Namespaces: namespaces, TypeName: "DaemonSet", AutoQueryExtract: true, - cacheField: cache, + cache: cache, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.DaemonSet, *v1.DaemonSetList] { return cs.AppsV1().DaemonSets(namespace) }, diff --git a/k8s-source/adapters/deployment.go b/k8s-source/adapters/deployment.go index 476c2a27..12916a45 100644 --- a/k8s-source/adapters/deployment.go +++ b/k8s-source/adapters/deployment.go @@ -19,7 +19,7 @@ func newDeploymentAdapter(cs *kubernetes.Clientset, cluster string, namespaces [ Namespaces: namespaces, TypeName: "Deployment", AutoQueryExtract: true, - cacheField: cache, + cache: cache, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Deployment, *v1.DeploymentList] { return cs.AppsV1().Deployments(namespace) }, diff --git a/k8s-source/adapters/endpointslice.go b/k8s-source/adapters/endpointslice.go index e21d07a1..de68e992 100644 --- a/k8s-source/adapters/endpointslice.go +++ b/k8s-source/adapters/endpointslice.go @@ -103,7 +103,7 @@ func newEndpointSliceAdapter(cs *kubernetes.Clientset, cluster string, namespace return &KubeTypeAdapter[*v1.EndpointSlice, *v1.EndpointSliceList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "EndpointSlice", CacheDuration: 1 * time.Minute, // very low since this changes a lot NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.EndpointSlice, *v1.EndpointSliceList] { diff --git a/k8s-source/adapters/generic_source.go b/k8s-source/adapters/generic_source.go index 44186ed2..0f1c0434 100644 --- a/k8s-source/adapters/generic_source.go +++ b/k8s-source/adapters/generic_source.go @@ -76,7 +76,7 @@ type KubeTypeAdapter[Resource metav1.Object, ResourceList any] struct { AdapterMetadata *sdp.AdapterMetadata CacheDuration time.Duration // How long to cache items for - cacheField sdpcache.Cache // The cache for this adapter (set during creation, can be nil for tests) + cache sdpcache.Cache // This is mandatory } func (s *KubeTypeAdapter[Resource, ResourceList]) cacheDuration() time.Duration { @@ -93,13 +93,13 @@ var ( ) func (s *KubeTypeAdapter[Resource, ResourceList]) Cache() sdpcache.Cache { - if s.cacheField == nil { + if s.cache == nil { noOpCacheK8sOnce.Do(func() { noOpCacheK8s = sdpcache.NewNoOpCache() }) return noOpCacheK8s } - return s.cacheField + return s.cache } // validate Validates that the adapter is correctly set up diff --git a/k8s-source/adapters/horizontalpodautoscaler.go b/k8s-source/adapters/horizontalpodautoscaler.go index 8460396e..db7d74ff 100644 --- a/k8s-source/adapters/horizontalpodautoscaler.go +++ b/k8s-source/adapters/horizontalpodautoscaler.go @@ -34,7 +34,7 @@ func newHorizontalPodAutoscalerAdapter(cs *kubernetes.Clientset, cluster string, return &KubeTypeAdapter[*v2.HorizontalPodAutoscaler, *v2.HorizontalPodAutoscalerList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "HorizontalPodAutoscaler", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v2.HorizontalPodAutoscaler, *v2.HorizontalPodAutoscalerList] { return cs.AutoscalingV2().HorizontalPodAutoscalers(namespace) diff --git a/k8s-source/adapters/ingress.go b/k8s-source/adapters/ingress.go index 21004ffe..45fab9f0 100644 --- a/k8s-source/adapters/ingress.go +++ b/k8s-source/adapters/ingress.go @@ -132,7 +132,7 @@ func newIngressAdapter(cs *kubernetes.Clientset, cluster string, namespaces []st return &KubeTypeAdapter[*v1.Ingress, *v1.IngressList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "Ingress", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Ingress, *v1.IngressList] { return cs.NetworkingV1().Ingresses(namespace) diff --git a/k8s-source/adapters/job.go b/k8s-source/adapters/job.go index 235c0cc8..fce17eec 100644 --- a/k8s-source/adapters/job.go +++ b/k8s-source/adapters/job.go @@ -36,7 +36,7 @@ func newJobAdapter(cs *kubernetes.Clientset, cluster string, namespaces []string return &KubeTypeAdapter[*v1.Job, *v1.JobList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "Job", AutoQueryExtract: true, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Job, *v1.JobList] { diff --git a/k8s-source/adapters/limitrange.go b/k8s-source/adapters/limitrange.go index 90840a88..a0c19298 100644 --- a/k8s-source/adapters/limitrange.go +++ b/k8s-source/adapters/limitrange.go @@ -12,7 +12,7 @@ func newLimitRangeAdapter(cs *kubernetes.Clientset, cluster string, namespaces [ return &KubeTypeAdapter[*v1.LimitRange, *v1.LimitRangeList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "LimitRange", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.LimitRange, *v1.LimitRangeList] { return cs.CoreV1().LimitRanges(namespace) diff --git a/k8s-source/adapters/networkpolicy.go b/k8s-source/adapters/networkpolicy.go index 7790d095..fa6b9810 100644 --- a/k8s-source/adapters/networkpolicy.go +++ b/k8s-source/adapters/networkpolicy.go @@ -71,7 +71,7 @@ func newNetworkPolicyAdapter(cs *kubernetes.Clientset, cluster string, namespace return &KubeTypeAdapter[*v1.NetworkPolicy, *v1.NetworkPolicyList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "NetworkPolicy", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.NetworkPolicy, *v1.NetworkPolicyList] { return cs.NetworkingV1().NetworkPolicies(namespace) diff --git a/k8s-source/adapters/node.go b/k8s-source/adapters/node.go index 974f83a2..4c066308 100644 --- a/k8s-source/adapters/node.go +++ b/k8s-source/adapters/node.go @@ -81,7 +81,7 @@ func newNodeAdapter(cs *kubernetes.Clientset, cluster string, namespaces []strin ClusterName: cluster, Namespaces: namespaces, TypeName: "Node", - cacheField: cache, + cache: cache, ClusterInterfaceBuilder: func() ItemInterface[*v1.Node, *v1.NodeList] { return cs.CoreV1().Nodes() }, diff --git a/k8s-source/adapters/persistentvolume.go b/k8s-source/adapters/persistentvolume.go index 42c1062f..6492dc05 100644 --- a/k8s-source/adapters/persistentvolume.go +++ b/k8s-source/adapters/persistentvolume.go @@ -96,7 +96,7 @@ func newPersistentVolumeAdapter(cs *kubernetes.Clientset, cluster string, namesp return &KubeTypeAdapter[*v1.PersistentVolume, *v1.PersistentVolumeList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "PersistentVolume", ClusterInterfaceBuilder: func() ItemInterface[*v1.PersistentVolume, *v1.PersistentVolumeList] { return cs.CoreV1().PersistentVolumes() diff --git a/k8s-source/adapters/persistentvolumeclaim.go b/k8s-source/adapters/persistentvolumeclaim.go index c94f6ef2..70e66384 100644 --- a/k8s-source/adapters/persistentvolumeclaim.go +++ b/k8s-source/adapters/persistentvolumeclaim.go @@ -42,7 +42,7 @@ func newPersistentVolumeClaimAdapter(cs *kubernetes.Clientset, cluster string, n return &KubeTypeAdapter[*v1.PersistentVolumeClaim, *v1.PersistentVolumeClaimList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "PersistentVolumeClaim", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.PersistentVolumeClaim, *v1.PersistentVolumeClaimList] { return cs.CoreV1().PersistentVolumeClaims(namespace) diff --git a/k8s-source/adapters/poddisruptionbudget.go b/k8s-source/adapters/poddisruptionbudget.go index 30aedcd2..12f0d58c 100644 --- a/k8s-source/adapters/poddisruptionbudget.go +++ b/k8s-source/adapters/poddisruptionbudget.go @@ -35,7 +35,7 @@ func newPodDisruptionBudgetAdapter(cs *kubernetes.Clientset, cluster string, nam return &KubeTypeAdapter[*v1.PodDisruptionBudget, *v1.PodDisruptionBudgetList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "PodDisruptionBudget", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.PodDisruptionBudget, *v1.PodDisruptionBudgetList] { return cs.PolicyV1().PodDisruptionBudgets(namespace) diff --git a/k8s-source/adapters/pods.go b/k8s-source/adapters/pods.go index d55eb6d7..d5e61013 100644 --- a/k8s-source/adapters/pods.go +++ b/k8s-source/adapters/pods.go @@ -332,7 +332,7 @@ func newPodAdapter(cs *kubernetes.Clientset, cluster string, namespaces []string TypeName: "Pod", CacheDuration: 10 * time.Minute, // somewhat low since pods are replaced a lot AutoQueryExtract: true, - cacheField: cache, + cache: cache, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Pod, *v1.PodList] { return cs.CoreV1().Pods(namespace) }, diff --git a/k8s-source/adapters/priorityclass.go b/k8s-source/adapters/priorityclass.go index 6fd6b0df..613c909d 100644 --- a/k8s-source/adapters/priorityclass.go +++ b/k8s-source/adapters/priorityclass.go @@ -13,7 +13,7 @@ func newPriorityClassAdapter(cs *kubernetes.Clientset, cluster string, namespace return &KubeTypeAdapter[*v1.PriorityClass, *v1.PriorityClassList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "PriorityClass", ClusterInterfaceBuilder: func() ItemInterface[*v1.PriorityClass, *v1.PriorityClassList] { return cs.SchedulingV1().PriorityClasses() diff --git a/k8s-source/adapters/replicaset.go b/k8s-source/adapters/replicaset.go index e900ceb1..e466fe0f 100644 --- a/k8s-source/adapters/replicaset.go +++ b/k8s-source/adapters/replicaset.go @@ -36,7 +36,7 @@ func newReplicaSetAdapter(cs *kubernetes.Clientset, cluster string, namespaces [ return &KubeTypeAdapter[*v1.ReplicaSet, *v1.ReplicaSetList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "ReplicaSet", AutoQueryExtract: true, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.ReplicaSet, *v1.ReplicaSetList] { diff --git a/k8s-source/adapters/replicationcontroller.go b/k8s-source/adapters/replicationcontroller.go index 8bdca813..f4f8bf19 100644 --- a/k8s-source/adapters/replicationcontroller.go +++ b/k8s-source/adapters/replicationcontroller.go @@ -38,7 +38,7 @@ func newReplicationControllerAdapter(cs *kubernetes.Clientset, cluster string, n return &KubeTypeAdapter[*v1.ReplicationController, *v1.ReplicationControllerList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "ReplicationController", AutoQueryExtract: true, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.ReplicationController, *v1.ReplicationControllerList] { diff --git a/k8s-source/adapters/resourcequota.go b/k8s-source/adapters/resourcequota.go index f51c392a..e0dbc30a 100644 --- a/k8s-source/adapters/resourcequota.go +++ b/k8s-source/adapters/resourcequota.go @@ -12,7 +12,7 @@ func newResourceQuotaAdapter(cs *kubernetes.Clientset, cluster string, namespace return &KubeTypeAdapter[*v1.ResourceQuota, *v1.ResourceQuotaList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "ResourceQuota", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.ResourceQuota, *v1.ResourceQuotaList] { return cs.CoreV1().ResourceQuotas(namespace) diff --git a/k8s-source/adapters/role.go b/k8s-source/adapters/role.go index fa15351c..52db8ecd 100644 --- a/k8s-source/adapters/role.go +++ b/k8s-source/adapters/role.go @@ -13,7 +13,7 @@ func newRoleAdapter(cs *kubernetes.Clientset, cluster string, namespaces []strin return &KubeTypeAdapter[*v1.Role, *v1.RoleList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "Role", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Role, *v1.RoleList] { return cs.RbacV1().Roles(namespace) diff --git a/k8s-source/adapters/rolebinding.go b/k8s-source/adapters/rolebinding.go index 3caa36a3..1e82d04a 100644 --- a/k8s-source/adapters/rolebinding.go +++ b/k8s-source/adapters/rolebinding.go @@ -77,7 +77,7 @@ func newRoleBindingAdapter(cs *kubernetes.Clientset, cluster string, namespaces return &KubeTypeAdapter[*v1.RoleBinding, *v1.RoleBindingList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "RoleBinding", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.RoleBinding, *v1.RoleBindingList] { return cs.RbacV1().RoleBindings(namespace) diff --git a/k8s-source/adapters/secret.go b/k8s-source/adapters/secret.go index d8a802fc..46e73b94 100644 --- a/k8s-source/adapters/secret.go +++ b/k8s-source/adapters/secret.go @@ -14,7 +14,7 @@ func newSecretAdapter(cs *kubernetes.Clientset, cluster string, namespaces []str return &KubeTypeAdapter[*v1.Secret, *v1.SecretList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "Secret", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Secret, *v1.SecretList] { return cs.CoreV1().Secrets(namespace) diff --git a/k8s-source/adapters/service.go b/k8s-source/adapters/service.go index 42081713..a88f65f4 100644 --- a/k8s-source/adapters/service.go +++ b/k8s-source/adapters/service.go @@ -134,7 +134,7 @@ func newServiceAdapter(cs *kubernetes.Clientset, cluster string, namespaces []st ClusterName: cluster, Namespaces: namespaces, TypeName: "Service", - cacheField: cache, + cache: cache, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Service, *v1.ServiceList] { return cs.CoreV1().Services(namespace) }, diff --git a/k8s-source/adapters/serviceaccount.go b/k8s-source/adapters/serviceaccount.go index 1385a56e..b7372430 100644 --- a/k8s-source/adapters/serviceaccount.go +++ b/k8s-source/adapters/serviceaccount.go @@ -54,7 +54,7 @@ func newServiceAccountAdapter(cs *kubernetes.Clientset, cluster string, namespac return &KubeTypeAdapter[*v1.ServiceAccount, *v1.ServiceAccountList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "ServiceAccount", NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.ServiceAccount, *v1.ServiceAccountList] { return cs.CoreV1().ServiceAccounts(namespace) diff --git a/k8s-source/adapters/statefulset.go b/k8s-source/adapters/statefulset.go index ed72f5ad..37f43264 100644 --- a/k8s-source/adapters/statefulset.go +++ b/k8s-source/adapters/statefulset.go @@ -71,7 +71,7 @@ func newStatefulSetAdapter(cs *kubernetes.Clientset, cluster string, namespaces return &KubeTypeAdapter[*v1.StatefulSet, *v1.StatefulSetList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "StatefulSet", AutoQueryExtract: true, NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.StatefulSet, *v1.StatefulSetList] { diff --git a/k8s-source/adapters/storageclass.go b/k8s-source/adapters/storageclass.go index 2707997a..8482d5d5 100644 --- a/k8s-source/adapters/storageclass.go +++ b/k8s-source/adapters/storageclass.go @@ -13,7 +13,7 @@ func newStorageClassAdapter(cs *kubernetes.Clientset, cluster string, namespaces return &KubeTypeAdapter[*v1.StorageClass, *v1.StorageClassList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "StorageClass", ClusterInterfaceBuilder: func() ItemInterface[*v1.StorageClass, *v1.StorageClassList] { return cs.StorageV1().StorageClasses() diff --git a/k8s-source/adapters/volumeattachment.go b/k8s-source/adapters/volumeattachment.go index 7d54b30a..ce15ab65 100644 --- a/k8s-source/adapters/volumeattachment.go +++ b/k8s-source/adapters/volumeattachment.go @@ -51,7 +51,7 @@ func newVolumeAttachmentAdapter(cs *kubernetes.Clientset, cluster string, namesp return &KubeTypeAdapter[*v1.VolumeAttachment, *v1.VolumeAttachmentList]{ ClusterName: cluster, Namespaces: namespaces, - cacheField: cache, + cache: cache, TypeName: "VolumeAttachment", ClusterInterfaceBuilder: func() ItemInterface[*v1.VolumeAttachment, *v1.VolumeAttachmentList] { return cs.StorageV1().VolumeAttachments() diff --git a/k8s-source/cmd/root.go b/k8s-source/cmd/root.go index 82b1147e..1645c4ee 100644 --- a/k8s-source/cmd/root.go +++ b/k8s-source/cmd/root.go @@ -181,35 +181,7 @@ func run(_ *cobra.Command, _ []string) int { // Start HTTP server for health checks healthCheckPort := viper.GetInt("health-check-port") - - log.WithFields(log.Fields{ - "port": healthCheckPort, - }).Debug("Starting healthcheck server with endpoints: /healthz/alive, /healthz/ready, /healthz") - - go func() { - defer sentry.Recover() - - mux := http.NewServeMux() - - // Liveness: Check only engine initialization (NATS, heartbeats) - mux.HandleFunc("/healthz/alive", e.LivenessProbeHandlerFunc()) - // Readiness: Check if adapters are healthy and ready to handle requests - mux.HandleFunc("/healthz/ready", e.ReadinessProbeHandlerFunc()) - // Backward compatibility - maps to liveness check (matches old behavior) - mux.HandleFunc("/healthz", e.LivenessProbeHandlerFunc()) - - server := &http.Server{ - Addr: fmt.Sprintf(":%v", healthCheckPort), - Handler: mux, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - err := server.ListenAndServe() - - log.WithError(err).WithFields(log.Fields{ - "port": healthCheckPort, - }).Error("Could not start HTTP server for health checks") - }() + e.ServeHealthProbes(healthCheckPort) // Create channels for interrupts quit := make(chan os.Signal, 1) diff --git a/sdp-go/changes.pb.go b/sdp-go/changes.pb.go index 0b387686..009b89ec 100644 --- a/sdp-go/changes.pb.go +++ b/sdp-go/changes.pb.go @@ -22,6 +22,59 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// Status of a mapped item in the timeline +type MappedItemTimelineStatus int32 + +const ( + MappedItemTimelineStatus_MAPPED_ITEM_TIMELINE_STATUS_UNSPECIFIED MappedItemTimelineStatus = 0 + MappedItemTimelineStatus_MAPPED_ITEM_TIMELINE_STATUS_SUCCESS MappedItemTimelineStatus = 1 + MappedItemTimelineStatus_MAPPED_ITEM_TIMELINE_STATUS_ERROR MappedItemTimelineStatus = 2 + MappedItemTimelineStatus_MAPPED_ITEM_TIMELINE_STATUS_UNSUPPORTED MappedItemTimelineStatus = 3 +) + +// Enum value maps for MappedItemTimelineStatus. +var ( + MappedItemTimelineStatus_name = map[int32]string{ + 0: "MAPPED_ITEM_TIMELINE_STATUS_UNSPECIFIED", + 1: "MAPPED_ITEM_TIMELINE_STATUS_SUCCESS", + 2: "MAPPED_ITEM_TIMELINE_STATUS_ERROR", + 3: "MAPPED_ITEM_TIMELINE_STATUS_UNSUPPORTED", + } + MappedItemTimelineStatus_value = map[string]int32{ + "MAPPED_ITEM_TIMELINE_STATUS_UNSPECIFIED": 0, + "MAPPED_ITEM_TIMELINE_STATUS_SUCCESS": 1, + "MAPPED_ITEM_TIMELINE_STATUS_ERROR": 2, + "MAPPED_ITEM_TIMELINE_STATUS_UNSUPPORTED": 3, + } +) + +func (x MappedItemTimelineStatus) Enum() *MappedItemTimelineStatus { + p := new(MappedItemTimelineStatus) + *p = x + return p +} + +func (x MappedItemTimelineStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MappedItemTimelineStatus) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[0].Descriptor() +} + +func (MappedItemTimelineStatus) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[0] +} + +func (x MappedItemTimelineStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MappedItemTimelineStatus.Descriptor instead. +func (MappedItemTimelineStatus) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{0} +} + type HypothesisStatus int32 const ( @@ -67,11 +120,11 @@ func (x HypothesisStatus) String() string { } func (HypothesisStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[0].Descriptor() + return file_changes_proto_enumTypes[1].Descriptor() } func (HypothesisStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[0] + return &file_changes_proto_enumTypes[1] } func (x HypothesisStatus) Number() protoreflect.EnumNumber { @@ -80,7 +133,7 @@ func (x HypothesisStatus) Number() protoreflect.EnumNumber { // Deprecated: Use HypothesisStatus.Descriptor instead. func (HypothesisStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{0} + return file_changes_proto_rawDescGZIP(), []int{1} } type ChangeTimelineEntryStatus int32 @@ -127,11 +180,11 @@ func (x ChangeTimelineEntryStatus) String() string { } func (ChangeTimelineEntryStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[1].Descriptor() + return file_changes_proto_enumTypes[2].Descriptor() } func (ChangeTimelineEntryStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[1] + return &file_changes_proto_enumTypes[2] } func (x ChangeTimelineEntryStatus) Number() protoreflect.EnumNumber { @@ -140,7 +193,7 @@ func (x ChangeTimelineEntryStatus) Number() protoreflect.EnumNumber { // Deprecated: Use ChangeTimelineEntryStatus.Descriptor instead. func (ChangeTimelineEntryStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{1} + return file_changes_proto_rawDescGZIP(), []int{2} } type ItemDiffStatus int32 @@ -185,11 +238,11 @@ func (x ItemDiffStatus) String() string { } func (ItemDiffStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[2].Descriptor() + return file_changes_proto_enumTypes[3].Descriptor() } func (ItemDiffStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[2] + return &file_changes_proto_enumTypes[3] } func (x ItemDiffStatus) Number() protoreflect.EnumNumber { @@ -198,7 +251,7 @@ func (x ItemDiffStatus) Number() protoreflect.EnumNumber { // Deprecated: Use ItemDiffStatus.Descriptor instead. func (ItemDiffStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{2} + return file_changes_proto_rawDescGZIP(), []int{3} } type ChangeOutputFormat int32 @@ -234,11 +287,11 @@ func (x ChangeOutputFormat) String() string { } func (ChangeOutputFormat) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[3].Descriptor() + return file_changes_proto_enumTypes[4].Descriptor() } func (ChangeOutputFormat) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[3] + return &file_changes_proto_enumTypes[4] } func (x ChangeOutputFormat) Number() protoreflect.EnumNumber { @@ -247,7 +300,7 @@ func (x ChangeOutputFormat) Number() protoreflect.EnumNumber { // Deprecated: Use ChangeOutputFormat.Descriptor instead. func (ChangeOutputFormat) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{3} + return file_changes_proto_rawDescGZIP(), []int{4} } type LabelType int32 @@ -283,11 +336,11 @@ func (x LabelType) String() string { } func (LabelType) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[4].Descriptor() + return file_changes_proto_enumTypes[5].Descriptor() } func (LabelType) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[4] + return &file_changes_proto_enumTypes[5] } func (x LabelType) Number() protoreflect.EnumNumber { @@ -296,7 +349,7 @@ func (x LabelType) Number() protoreflect.EnumNumber { // Deprecated: Use LabelType.Descriptor instead. func (LabelType) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{4} + return file_changes_proto_rawDescGZIP(), []int{5} } type ChangeStatus int32 @@ -350,11 +403,11 @@ func (x ChangeStatus) String() string { } func (ChangeStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[5].Descriptor() + return file_changes_proto_enumTypes[6].Descriptor() } func (ChangeStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[5] + return &file_changes_proto_enumTypes[6] } func (x ChangeStatus) Number() protoreflect.EnumNumber { @@ -363,7 +416,7 @@ func (x ChangeStatus) Number() protoreflect.EnumNumber { // Deprecated: Use ChangeStatus.Descriptor instead. func (ChangeStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{5} + return file_changes_proto_rawDescGZIP(), []int{6} } type StartChangeResponse_State int32 @@ -406,11 +459,11 @@ func (x StartChangeResponse_State) String() string { } func (StartChangeResponse_State) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[6].Descriptor() + return file_changes_proto_enumTypes[7].Descriptor() } func (StartChangeResponse_State) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[6] + return &file_changes_proto_enumTypes[7] } func (x StartChangeResponse_State) Number() protoreflect.EnumNumber { @@ -419,7 +472,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{83, 0} + return file_changes_proto_rawDescGZIP(), []int{84, 0} } type EndChangeResponse_State int32 @@ -462,11 +515,11 @@ func (x EndChangeResponse_State) String() string { } func (EndChangeResponse_State) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[7].Descriptor() + return file_changes_proto_enumTypes[8].Descriptor() } func (EndChangeResponse_State) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[7] + return &file_changes_proto_enumTypes[8] } func (x EndChangeResponse_State) Number() protoreflect.EnumNumber { @@ -475,7 +528,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{85, 0} + return file_changes_proto_rawDescGZIP(), []int{86, 0} } type Risk_Severity int32 @@ -514,11 +567,11 @@ func (x Risk_Severity) String() string { } func (Risk_Severity) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[8].Descriptor() + return file_changes_proto_enumTypes[9].Descriptor() } func (Risk_Severity) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[8] + return &file_changes_proto_enumTypes[9] } func (x Risk_Severity) Number() protoreflect.EnumNumber { @@ -527,7 +580,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{86, 0} + return file_changes_proto_rawDescGZIP(), []int{87, 0} } type ChangeAnalysisStatus_Status int32 @@ -569,11 +622,11 @@ func (x ChangeAnalysisStatus_Status) String() string { } func (ChangeAnalysisStatus_Status) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[9].Descriptor() + return file_changes_proto_enumTypes[10].Descriptor() } func (ChangeAnalysisStatus_Status) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[9] + return &file_changes_proto_enumTypes[10] } func (x ChangeAnalysisStatus_Status) Number() protoreflect.EnumNumber { @@ -582,7 +635,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{87, 0} + return file_changes_proto_rawDescGZIP(), []int{88, 0} } type LabelRule struct { @@ -2010,25 +2063,87 @@ func (*EmptyContent) Descriptor() ([]byte, []int) { return file_changes_proto_rawDescGZIP(), []int{23} } +// Per-item summary for timeline display - only what the UI needs +type MappedItemTimelineSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The display name (unique attribute value) shown to the user + DisplayName string `protobuf:"bytes,1,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // The status of the mapping result + Status MappedItemTimelineStatus `protobuf:"varint,2,opt,name=status,proto3,enum=changes.MappedItemTimelineStatus" json:"status,omitempty"` + // Only populated when status == ERROR + ErrorMessage *string `protobuf:"bytes,3,opt,name=error_message,json=errorMessage,proto3,oneof" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MappedItemTimelineSummary) Reset() { + *x = MappedItemTimelineSummary{} + mi := &file_changes_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MappedItemTimelineSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MappedItemTimelineSummary) ProtoMessage() {} + +func (x *MappedItemTimelineSummary) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[24] + 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 MappedItemTimelineSummary.ProtoReflect.Descriptor instead. +func (*MappedItemTimelineSummary) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{24} +} + +func (x *MappedItemTimelineSummary) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *MappedItemTimelineSummary) GetStatus() MappedItemTimelineStatus { + if x != nil { + return x.Status + } + return MappedItemTimelineStatus_MAPPED_ITEM_TIMELINE_STATUS_UNSPECIFIED +} + +func (x *MappedItemTimelineSummary) GetErrorMessage() string { + if x != nil && x.ErrorMessage != nil { + return *x.ErrorMessage + } + return "" +} + type MappedItemsTimelineEntry struct { state protoimpl.MessageState `protogen:"open.v1"` - // This is the result of Overmind trying to map a changing item to something - // real within the environment. There are 3 possible ways this can go: + // Deprecated: This field is for backwards compatibility with old change archives. + // When unmarshaling old archives with field number 1, this will be populated. + // The timeline is reconstructed from the database anyway, so this data is ignored. // - // 1. The change was submitted without a mapping query and so there was no way - // for us to find the corresponding item. - // 2. The change was submitted with a mapping query and we were able to find the - // item that the submitter was referring to. - // 3. The change was submitted with a mapping query and we were unable to find - // the item that the submitter was referring to. - MappedItems []*MappedItemDiff `protobuf:"bytes,1,rep,name=mappedItems,proto3" json:"mappedItems,omitempty"` + // Deprecated: Marked as deprecated in changes.proto. + MappedItems []*MappedItemDiff `protobuf:"bytes,1,rep,name=mappedItems,proto3" json:"mappedItems,omitempty"` + // New simplified timeline summary - only what the UI needs + Items []*MappedItemTimelineSummary `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *MappedItemsTimelineEntry) Reset() { *x = MappedItemsTimelineEntry{} - mi := &file_changes_proto_msgTypes[24] + mi := &file_changes_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2040,7 +2155,7 @@ func (x *MappedItemsTimelineEntry) String() string { func (*MappedItemsTimelineEntry) ProtoMessage() {} func (x *MappedItemsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[24] + mi := &file_changes_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2053,9 +2168,10 @@ func (x *MappedItemsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedItemsTimelineEntry.ProtoReflect.Descriptor instead. func (*MappedItemsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{24} + return file_changes_proto_rawDescGZIP(), []int{25} } +// Deprecated: Marked as deprecated in changes.proto. func (x *MappedItemsTimelineEntry) GetMappedItems() []*MappedItemDiff { if x != nil { return x.MappedItems @@ -2063,6 +2179,13 @@ func (x *MappedItemsTimelineEntry) GetMappedItems() []*MappedItemDiff { return nil } +func (x *MappedItemsTimelineEntry) GetItems() []*MappedItemTimelineSummary { + if x != nil { + return x.Items + } + return nil +} + type CalculatedBlastRadiusTimelineEntry struct { state protoimpl.MessageState `protogen:"open.v1"` NumItems uint32 `protobuf:"varint,1,opt,name=numItems,proto3" json:"numItems,omitempty"` @@ -2073,7 +2196,7 @@ type CalculatedBlastRadiusTimelineEntry struct { func (x *CalculatedBlastRadiusTimelineEntry) Reset() { *x = CalculatedBlastRadiusTimelineEntry{} - mi := &file_changes_proto_msgTypes[25] + mi := &file_changes_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2085,7 +2208,7 @@ func (x *CalculatedBlastRadiusTimelineEntry) String() string { func (*CalculatedBlastRadiusTimelineEntry) ProtoMessage() {} func (x *CalculatedBlastRadiusTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[25] + mi := &file_changes_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2098,7 +2221,7 @@ func (x *CalculatedBlastRadiusTimelineEntry) ProtoReflect() protoreflect.Message // Deprecated: Use CalculatedBlastRadiusTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedBlastRadiusTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{25} + return file_changes_proto_rawDescGZIP(), []int{26} } func (x *CalculatedBlastRadiusTimelineEntry) GetNumItems() uint32 { @@ -2126,7 +2249,7 @@ type RecordObservationsTimelineEntry struct { func (x *RecordObservationsTimelineEntry) Reset() { *x = RecordObservationsTimelineEntry{} - mi := &file_changes_proto_msgTypes[26] + mi := &file_changes_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2138,7 +2261,7 @@ func (x *RecordObservationsTimelineEntry) String() string { func (*RecordObservationsTimelineEntry) ProtoMessage() {} func (x *RecordObservationsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[26] + mi := &file_changes_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2151,7 +2274,7 @@ func (x *RecordObservationsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordObservationsTimelineEntry.ProtoReflect.Descriptor instead. func (*RecordObservationsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{26} + return file_changes_proto_rawDescGZIP(), []int{27} } func (x *RecordObservationsTimelineEntry) GetNumObservations() uint32 { @@ -2174,7 +2297,7 @@ type FormHypothesesTimelineEntry struct { func (x *FormHypothesesTimelineEntry) Reset() { *x = FormHypothesesTimelineEntry{} - mi := &file_changes_proto_msgTypes[27] + mi := &file_changes_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2186,7 +2309,7 @@ func (x *FormHypothesesTimelineEntry) String() string { func (*FormHypothesesTimelineEntry) ProtoMessage() {} func (x *FormHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[27] + mi := &file_changes_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2199,7 +2322,7 @@ func (x *FormHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use FormHypothesesTimelineEntry.ProtoReflect.Descriptor instead. func (*FormHypothesesTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{27} + return file_changes_proto_rawDescGZIP(), []int{28} } func (x *FormHypothesesTimelineEntry) GetNumHypotheses() uint32 { @@ -2232,7 +2355,7 @@ type InvestigateHypothesesTimelineEntry struct { func (x *InvestigateHypothesesTimelineEntry) Reset() { *x = InvestigateHypothesesTimelineEntry{} - mi := &file_changes_proto_msgTypes[28] + mi := &file_changes_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2244,7 +2367,7 @@ func (x *InvestigateHypothesesTimelineEntry) String() string { func (*InvestigateHypothesesTimelineEntry) ProtoMessage() {} func (x *InvestigateHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[28] + mi := &file_changes_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2257,7 +2380,7 @@ func (x *InvestigateHypothesesTimelineEntry) ProtoReflect() protoreflect.Message // Deprecated: Use InvestigateHypothesesTimelineEntry.ProtoReflect.Descriptor instead. func (*InvestigateHypothesesTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{28} + return file_changes_proto_rawDescGZIP(), []int{29} } func (x *InvestigateHypothesesTimelineEntry) GetNumProven() uint32 { @@ -2306,7 +2429,7 @@ type HypothesisSummary struct { func (x *HypothesisSummary) Reset() { *x = HypothesisSummary{} - mi := &file_changes_proto_msgTypes[29] + mi := &file_changes_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2318,7 +2441,7 @@ func (x *HypothesisSummary) String() string { func (*HypothesisSummary) ProtoMessage() {} func (x *HypothesisSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[29] + mi := &file_changes_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2331,7 +2454,7 @@ func (x *HypothesisSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use HypothesisSummary.ProtoReflect.Descriptor instead. func (*HypothesisSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{29} + return file_changes_proto_rawDescGZIP(), []int{30} } func (x *HypothesisSummary) GetStatus() HypothesisStatus { @@ -2364,7 +2487,7 @@ type CalculatedRisksTimelineEntry struct { func (x *CalculatedRisksTimelineEntry) Reset() { *x = CalculatedRisksTimelineEntry{} - mi := &file_changes_proto_msgTypes[30] + mi := &file_changes_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2376,7 +2499,7 @@ func (x *CalculatedRisksTimelineEntry) String() string { func (*CalculatedRisksTimelineEntry) ProtoMessage() {} func (x *CalculatedRisksTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[30] + mi := &file_changes_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2389,7 +2512,7 @@ func (x *CalculatedRisksTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use CalculatedRisksTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedRisksTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{30} + return file_changes_proto_rawDescGZIP(), []int{31} } func (x *CalculatedRisksTimelineEntry) GetRisks() []*Risk { @@ -2409,7 +2532,7 @@ type CalculatedLabelsTimelineEntry struct { func (x *CalculatedLabelsTimelineEntry) Reset() { *x = CalculatedLabelsTimelineEntry{} - mi := &file_changes_proto_msgTypes[31] + mi := &file_changes_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2421,7 +2544,7 @@ func (x *CalculatedLabelsTimelineEntry) String() string { func (*CalculatedLabelsTimelineEntry) ProtoMessage() {} func (x *CalculatedLabelsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[31] + mi := &file_changes_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2434,7 +2557,7 @@ func (x *CalculatedLabelsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use CalculatedLabelsTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedLabelsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{31} + return file_changes_proto_rawDescGZIP(), []int{32} } func (x *CalculatedLabelsTimelineEntry) GetLabels() []*Label { @@ -2457,7 +2580,7 @@ type ChangeValidationTimelineEntry struct { func (x *ChangeValidationTimelineEntry) Reset() { *x = ChangeValidationTimelineEntry{} - mi := &file_changes_proto_msgTypes[32] + mi := &file_changes_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2469,7 +2592,7 @@ func (x *ChangeValidationTimelineEntry) String() string { func (*ChangeValidationTimelineEntry) ProtoMessage() {} func (x *ChangeValidationTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[32] + mi := &file_changes_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2482,7 +2605,7 @@ func (x *ChangeValidationTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeValidationTimelineEntry.ProtoReflect.Descriptor instead. func (*ChangeValidationTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{32} + return file_changes_proto_rawDescGZIP(), []int{33} } func (x *ChangeValidationTimelineEntry) GetBriefAnalysis() string { @@ -2514,7 +2637,7 @@ type ChangeValidationCategory struct { func (x *ChangeValidationCategory) Reset() { *x = ChangeValidationCategory{} - mi := &file_changes_proto_msgTypes[33] + mi := &file_changes_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2526,7 +2649,7 @@ func (x *ChangeValidationCategory) String() string { func (*ChangeValidationCategory) ProtoMessage() {} func (x *ChangeValidationCategory) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[33] + mi := &file_changes_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2539,7 +2662,7 @@ func (x *ChangeValidationCategory) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeValidationCategory.ProtoReflect.Descriptor instead. func (*ChangeValidationCategory) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{33} + return file_changes_proto_rawDescGZIP(), []int{34} } func (x *ChangeValidationCategory) GetTitle() string { @@ -2565,7 +2688,7 @@ type GetDiffRequest struct { func (x *GetDiffRequest) Reset() { *x = GetDiffRequest{} - mi := &file_changes_proto_msgTypes[34] + mi := &file_changes_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2577,7 +2700,7 @@ func (x *GetDiffRequest) String() string { func (*GetDiffRequest) ProtoMessage() {} func (x *GetDiffRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[34] + mi := &file_changes_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2590,7 +2713,7 @@ func (x *GetDiffRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiffRequest.ProtoReflect.Descriptor instead. func (*GetDiffRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{34} + return file_changes_proto_rawDescGZIP(), []int{35} } func (x *GetDiffRequest) GetChangeUUID() []byte { @@ -2615,7 +2738,7 @@ type GetDiffResponse struct { func (x *GetDiffResponse) Reset() { *x = GetDiffResponse{} - mi := &file_changes_proto_msgTypes[35] + mi := &file_changes_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2627,7 +2750,7 @@ func (x *GetDiffResponse) String() string { func (*GetDiffResponse) ProtoMessage() {} func (x *GetDiffResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[35] + mi := &file_changes_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2640,7 +2763,7 @@ func (x *GetDiffResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiffResponse.ProtoReflect.Descriptor instead. func (*GetDiffResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{35} + return file_changes_proto_rawDescGZIP(), []int{36} } func (x *GetDiffResponse) GetExpectedItems() []*ItemDiff { @@ -2680,7 +2803,7 @@ type ListChangingItemsSummaryRequest struct { func (x *ListChangingItemsSummaryRequest) Reset() { *x = ListChangingItemsSummaryRequest{} - mi := &file_changes_proto_msgTypes[36] + mi := &file_changes_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2692,7 +2815,7 @@ func (x *ListChangingItemsSummaryRequest) String() string { func (*ListChangingItemsSummaryRequest) ProtoMessage() {} func (x *ListChangingItemsSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[36] + mi := &file_changes_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2705,7 +2828,7 @@ func (x *ListChangingItemsSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangingItemsSummaryRequest.ProtoReflect.Descriptor instead. func (*ListChangingItemsSummaryRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{36} + return file_changes_proto_rawDescGZIP(), []int{37} } func (x *ListChangingItemsSummaryRequest) GetChangeUUID() []byte { @@ -2724,7 +2847,7 @@ type ListChangingItemsSummaryResponse struct { func (x *ListChangingItemsSummaryResponse) Reset() { *x = ListChangingItemsSummaryResponse{} - mi := &file_changes_proto_msgTypes[37] + mi := &file_changes_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2736,7 +2859,7 @@ func (x *ListChangingItemsSummaryResponse) String() string { func (*ListChangingItemsSummaryResponse) ProtoMessage() {} func (x *ListChangingItemsSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[37] + mi := &file_changes_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2749,7 +2872,7 @@ func (x *ListChangingItemsSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangingItemsSummaryResponse.ProtoReflect.Descriptor instead. func (*ListChangingItemsSummaryResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{37} + return file_changes_proto_rawDescGZIP(), []int{38} } func (x *ListChangingItemsSummaryResponse) GetItems() []*ItemDiffSummary { @@ -2775,7 +2898,7 @@ type MappedItemDiff struct { func (x *MappedItemDiff) Reset() { *x = MappedItemDiff{} - mi := &file_changes_proto_msgTypes[38] + mi := &file_changes_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2787,7 +2910,7 @@ func (x *MappedItemDiff) String() string { func (*MappedItemDiff) ProtoMessage() {} func (x *MappedItemDiff) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[38] + mi := &file_changes_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2800,7 +2923,7 @@ func (x *MappedItemDiff) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedItemDiff.ProtoReflect.Descriptor instead. func (*MappedItemDiff) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{38} + return file_changes_proto_rawDescGZIP(), []int{39} } func (x *MappedItemDiff) GetItem() *ItemDiff { @@ -2846,7 +2969,7 @@ type StartChangeAnalysisRequest struct { func (x *StartChangeAnalysisRequest) Reset() { *x = StartChangeAnalysisRequest{} - mi := &file_changes_proto_msgTypes[39] + mi := &file_changes_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2858,7 +2981,7 @@ func (x *StartChangeAnalysisRequest) String() string { func (*StartChangeAnalysisRequest) ProtoMessage() {} func (x *StartChangeAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[39] + mi := &file_changes_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2871,7 +2994,7 @@ func (x *StartChangeAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeAnalysisRequest.ProtoReflect.Descriptor instead. func (*StartChangeAnalysisRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{39} + return file_changes_proto_rawDescGZIP(), []int{40} } func (x *StartChangeAnalysisRequest) GetChangeUUID() []byte { @@ -2919,7 +3042,7 @@ type StartChangeAnalysisResponse struct { func (x *StartChangeAnalysisResponse) Reset() { *x = StartChangeAnalysisResponse{} - mi := &file_changes_proto_msgTypes[40] + mi := &file_changes_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2931,7 +3054,7 @@ func (x *StartChangeAnalysisResponse) String() string { func (*StartChangeAnalysisResponse) ProtoMessage() {} func (x *StartChangeAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[40] + mi := &file_changes_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2944,7 +3067,7 @@ func (x *StartChangeAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeAnalysisResponse.ProtoReflect.Descriptor instead. func (*StartChangeAnalysisResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{40} + return file_changes_proto_rawDescGZIP(), []int{41} } type ListHomeChangesRequest struct { @@ -2957,7 +3080,7 @@ type ListHomeChangesRequest struct { func (x *ListHomeChangesRequest) Reset() { *x = ListHomeChangesRequest{} - mi := &file_changes_proto_msgTypes[41] + mi := &file_changes_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2969,7 +3092,7 @@ func (x *ListHomeChangesRequest) String() string { func (*ListHomeChangesRequest) ProtoMessage() {} func (x *ListHomeChangesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[41] + mi := &file_changes_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2982,7 +3105,7 @@ func (x *ListHomeChangesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHomeChangesRequest.ProtoReflect.Descriptor instead. func (*ListHomeChangesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{41} + return file_changes_proto_rawDescGZIP(), []int{42} } func (x *ListHomeChangesRequest) GetPagination() *PaginationRequest { @@ -3016,7 +3139,7 @@ type ChangeFiltersRequest struct { func (x *ChangeFiltersRequest) Reset() { *x = ChangeFiltersRequest{} - mi := &file_changes_proto_msgTypes[42] + mi := &file_changes_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3028,7 +3151,7 @@ func (x *ChangeFiltersRequest) String() string { func (*ChangeFiltersRequest) ProtoMessage() {} func (x *ChangeFiltersRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[42] + mi := &file_changes_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3041,7 +3164,7 @@ func (x *ChangeFiltersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeFiltersRequest.ProtoReflect.Descriptor instead. func (*ChangeFiltersRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{42} + return file_changes_proto_rawDescGZIP(), []int{43} } func (x *ChangeFiltersRequest) GetRepos() []string { @@ -3096,7 +3219,7 @@ type ListHomeChangesResponse struct { func (x *ListHomeChangesResponse) Reset() { *x = ListHomeChangesResponse{} - mi := &file_changes_proto_msgTypes[43] + mi := &file_changes_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3108,7 +3231,7 @@ func (x *ListHomeChangesResponse) String() string { func (*ListHomeChangesResponse) ProtoMessage() {} func (x *ListHomeChangesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[43] + mi := &file_changes_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3121,7 +3244,7 @@ func (x *ListHomeChangesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHomeChangesResponse.ProtoReflect.Descriptor instead. func (*ListHomeChangesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{43} + return file_changes_proto_rawDescGZIP(), []int{44} } func (x *ListHomeChangesResponse) GetChanges() []*ChangeSummary { @@ -3146,7 +3269,7 @@ type PopulateChangeFiltersRequest struct { func (x *PopulateChangeFiltersRequest) Reset() { *x = PopulateChangeFiltersRequest{} - mi := &file_changes_proto_msgTypes[44] + mi := &file_changes_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3158,7 +3281,7 @@ func (x *PopulateChangeFiltersRequest) String() string { func (*PopulateChangeFiltersRequest) ProtoMessage() {} func (x *PopulateChangeFiltersRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[44] + mi := &file_changes_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3171,7 +3294,7 @@ func (x *PopulateChangeFiltersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PopulateChangeFiltersRequest.ProtoReflect.Descriptor instead. func (*PopulateChangeFiltersRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{44} + return file_changes_proto_rawDescGZIP(), []int{45} } type PopulateChangeFiltersResponse struct { @@ -3184,7 +3307,7 @@ type PopulateChangeFiltersResponse struct { func (x *PopulateChangeFiltersResponse) Reset() { *x = PopulateChangeFiltersResponse{} - mi := &file_changes_proto_msgTypes[45] + mi := &file_changes_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3196,7 +3319,7 @@ func (x *PopulateChangeFiltersResponse) String() string { func (*PopulateChangeFiltersResponse) ProtoMessage() {} func (x *PopulateChangeFiltersResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[45] + mi := &file_changes_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3209,7 +3332,7 @@ func (x *PopulateChangeFiltersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PopulateChangeFiltersResponse.ProtoReflect.Descriptor instead. func (*PopulateChangeFiltersResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{45} + return file_changes_proto_rawDescGZIP(), []int{46} } func (x *PopulateChangeFiltersResponse) GetRepos() []string { @@ -3240,7 +3363,7 @@ type ItemDiffSummary struct { func (x *ItemDiffSummary) Reset() { *x = ItemDiffSummary{} - mi := &file_changes_proto_msgTypes[46] + mi := &file_changes_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3252,7 +3375,7 @@ func (x *ItemDiffSummary) String() string { func (*ItemDiffSummary) ProtoMessage() {} func (x *ItemDiffSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[46] + mi := &file_changes_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3265,7 +3388,7 @@ func (x *ItemDiffSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use ItemDiffSummary.ProtoReflect.Descriptor instead. func (*ItemDiffSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{46} + return file_changes_proto_rawDescGZIP(), []int{47} } func (x *ItemDiffSummary) GetItem() *Reference { @@ -3307,7 +3430,7 @@ type ItemDiff struct { func (x *ItemDiff) Reset() { *x = ItemDiff{} - mi := &file_changes_proto_msgTypes[47] + mi := &file_changes_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3319,7 +3442,7 @@ func (x *ItemDiff) String() string { func (*ItemDiff) ProtoMessage() {} func (x *ItemDiff) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[47] + mi := &file_changes_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3332,7 +3455,7 @@ func (x *ItemDiff) ProtoReflect() protoreflect.Message { // Deprecated: Use ItemDiff.ProtoReflect.Descriptor instead. func (*ItemDiff) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{47} + return file_changes_proto_rawDescGZIP(), []int{48} } func (x *ItemDiff) GetItem() *Reference { @@ -3379,7 +3502,7 @@ type EnrichedTags struct { func (x *EnrichedTags) Reset() { *x = EnrichedTags{} - mi := &file_changes_proto_msgTypes[48] + mi := &file_changes_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3391,7 +3514,7 @@ func (x *EnrichedTags) String() string { func (*EnrichedTags) ProtoMessage() {} func (x *EnrichedTags) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[48] + mi := &file_changes_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3404,7 +3527,7 @@ func (x *EnrichedTags) ProtoReflect() protoreflect.Message { // Deprecated: Use EnrichedTags.ProtoReflect.Descriptor instead. func (*EnrichedTags) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{48} + return file_changes_proto_rawDescGZIP(), []int{49} } func (x *EnrichedTags) GetTagValue() map[string]*TagValue { @@ -3429,7 +3552,7 @@ type TagValue struct { func (x *TagValue) Reset() { *x = TagValue{} - mi := &file_changes_proto_msgTypes[49] + mi := &file_changes_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3441,7 +3564,7 @@ func (x *TagValue) String() string { func (*TagValue) ProtoMessage() {} func (x *TagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[49] + mi := &file_changes_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3454,7 +3577,7 @@ func (x *TagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use TagValue.ProtoReflect.Descriptor instead. func (*TagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{49} + return file_changes_proto_rawDescGZIP(), []int{50} } func (x *TagValue) GetValue() isTagValue_Value { @@ -3508,7 +3631,7 @@ type UserTagValue struct { func (x *UserTagValue) Reset() { *x = UserTagValue{} - mi := &file_changes_proto_msgTypes[50] + mi := &file_changes_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3520,7 +3643,7 @@ func (x *UserTagValue) String() string { func (*UserTagValue) ProtoMessage() {} func (x *UserTagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[50] + mi := &file_changes_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3533,7 +3656,7 @@ func (x *UserTagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use UserTagValue.ProtoReflect.Descriptor instead. func (*UserTagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{50} + return file_changes_proto_rawDescGZIP(), []int{51} } func (x *UserTagValue) GetValue() string { @@ -3555,7 +3678,7 @@ type AutoTagValue struct { func (x *AutoTagValue) Reset() { *x = AutoTagValue{} - mi := &file_changes_proto_msgTypes[51] + mi := &file_changes_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3567,7 +3690,7 @@ func (x *AutoTagValue) String() string { func (*AutoTagValue) ProtoMessage() {} func (x *AutoTagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[51] + mi := &file_changes_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3580,7 +3703,7 @@ func (x *AutoTagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use AutoTagValue.ProtoReflect.Descriptor instead. func (*AutoTagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{51} + return file_changes_proto_rawDescGZIP(), []int{52} } func (x *AutoTagValue) GetValue() string { @@ -3623,7 +3746,7 @@ type Label struct { func (x *Label) Reset() { *x = Label{} - mi := &file_changes_proto_msgTypes[52] + mi := &file_changes_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3635,7 +3758,7 @@ func (x *Label) String() string { func (*Label) ProtoMessage() {} func (x *Label) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[52] + mi := &file_changes_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3648,7 +3771,7 @@ func (x *Label) ProtoReflect() protoreflect.Message { // Deprecated: Use Label.ProtoReflect.Descriptor instead. func (*Label) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{52} + return file_changes_proto_rawDescGZIP(), []int{53} } func (x *Label) GetType() LabelType { @@ -3747,7 +3870,7 @@ type ChangeSummary struct { func (x *ChangeSummary) Reset() { *x = ChangeSummary{} - mi := &file_changes_proto_msgTypes[53] + mi := &file_changes_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3759,7 +3882,7 @@ func (x *ChangeSummary) String() string { func (*ChangeSummary) ProtoMessage() {} func (x *ChangeSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[53] + mi := &file_changes_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3772,7 +3895,7 @@ func (x *ChangeSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeSummary.ProtoReflect.Descriptor instead. func (*ChangeSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{53} + return file_changes_proto_rawDescGZIP(), []int{54} } func (x *ChangeSummary) GetUUID() []byte { @@ -3915,7 +4038,7 @@ type Change struct { func (x *Change) Reset() { *x = Change{} - mi := &file_changes_proto_msgTypes[54] + mi := &file_changes_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3927,7 +4050,7 @@ func (x *Change) String() string { func (*Change) ProtoMessage() {} func (x *Change) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[54] + mi := &file_changes_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3940,7 +4063,7 @@ func (x *Change) ProtoReflect() protoreflect.Message { // Deprecated: Use Change.ProtoReflect.Descriptor instead. func (*Change) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{54} + return file_changes_proto_rawDescGZIP(), []int{55} } func (x *Change) GetMetadata() *ChangeMetadata { @@ -4006,7 +4129,7 @@ type ChangeMetadata struct { func (x *ChangeMetadata) Reset() { *x = ChangeMetadata{} - mi := &file_changes_proto_msgTypes[55] + mi := &file_changes_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4018,7 +4141,7 @@ func (x *ChangeMetadata) String() string { func (*ChangeMetadata) ProtoMessage() {} func (x *ChangeMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[55] + mi := &file_changes_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4031,7 +4154,7 @@ func (x *ChangeMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeMetadata.ProtoReflect.Descriptor instead. func (*ChangeMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{55} + return file_changes_proto_rawDescGZIP(), []int{56} } func (x *ChangeMetadata) GetUUID() []byte { @@ -4229,7 +4352,7 @@ type ChangeProperties struct { func (x *ChangeProperties) Reset() { *x = ChangeProperties{} - mi := &file_changes_proto_msgTypes[56] + mi := &file_changes_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4241,7 +4364,7 @@ func (x *ChangeProperties) String() string { func (*ChangeProperties) ProtoMessage() {} func (x *ChangeProperties) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[56] + mi := &file_changes_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4254,7 +4377,7 @@ func (x *ChangeProperties) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeProperties.ProtoReflect.Descriptor instead. func (*ChangeProperties) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{56} + return file_changes_proto_rawDescGZIP(), []int{57} } func (x *ChangeProperties) GetTitle() string { @@ -4388,7 +4511,7 @@ type GithubChangeInfo struct { func (x *GithubChangeInfo) Reset() { *x = GithubChangeInfo{} - mi := &file_changes_proto_msgTypes[57] + mi := &file_changes_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4400,7 +4523,7 @@ func (x *GithubChangeInfo) String() string { func (*GithubChangeInfo) ProtoMessage() {} func (x *GithubChangeInfo) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[57] + mi := &file_changes_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4413,7 +4536,7 @@ func (x *GithubChangeInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use GithubChangeInfo.ProtoReflect.Descriptor instead. func (*GithubChangeInfo) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{57} + return file_changes_proto_rawDescGZIP(), []int{58} } func (x *GithubChangeInfo) GetAuthorUsername() string { @@ -4453,7 +4576,7 @@ type ListChangesRequest struct { func (x *ListChangesRequest) Reset() { *x = ListChangesRequest{} - mi := &file_changes_proto_msgTypes[58] + mi := &file_changes_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4465,7 +4588,7 @@ func (x *ListChangesRequest) String() string { func (*ListChangesRequest) ProtoMessage() {} func (x *ListChangesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[58] + mi := &file_changes_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4478,7 +4601,7 @@ func (x *ListChangesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesRequest.ProtoReflect.Descriptor instead. func (*ListChangesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{58} + return file_changes_proto_rawDescGZIP(), []int{59} } type ListChangesResponse struct { @@ -4490,7 +4613,7 @@ type ListChangesResponse struct { func (x *ListChangesResponse) Reset() { *x = ListChangesResponse{} - mi := &file_changes_proto_msgTypes[59] + mi := &file_changes_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4502,7 +4625,7 @@ func (x *ListChangesResponse) String() string { func (*ListChangesResponse) ProtoMessage() {} func (x *ListChangesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[59] + mi := &file_changes_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4515,7 +4638,7 @@ func (x *ListChangesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesResponse.ProtoReflect.Descriptor instead. func (*ListChangesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{59} + return file_changes_proto_rawDescGZIP(), []int{60} } func (x *ListChangesResponse) GetChanges() []*Change { @@ -4535,7 +4658,7 @@ type ListChangesByStatusRequest struct { func (x *ListChangesByStatusRequest) Reset() { *x = ListChangesByStatusRequest{} - mi := &file_changes_proto_msgTypes[60] + mi := &file_changes_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4547,7 +4670,7 @@ func (x *ListChangesByStatusRequest) String() string { func (*ListChangesByStatusRequest) ProtoMessage() {} func (x *ListChangesByStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[60] + mi := &file_changes_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4560,7 +4683,7 @@ func (x *ListChangesByStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesByStatusRequest.ProtoReflect.Descriptor instead. func (*ListChangesByStatusRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{60} + return file_changes_proto_rawDescGZIP(), []int{61} } func (x *ListChangesByStatusRequest) GetStatus() ChangeStatus { @@ -4579,7 +4702,7 @@ type ListChangesByStatusResponse struct { func (x *ListChangesByStatusResponse) Reset() { *x = ListChangesByStatusResponse{} - mi := &file_changes_proto_msgTypes[61] + mi := &file_changes_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4591,7 +4714,7 @@ func (x *ListChangesByStatusResponse) String() string { func (*ListChangesByStatusResponse) ProtoMessage() {} func (x *ListChangesByStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[61] + mi := &file_changes_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4604,7 +4727,7 @@ func (x *ListChangesByStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesByStatusResponse.ProtoReflect.Descriptor instead. func (*ListChangesByStatusResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{61} + return file_changes_proto_rawDescGZIP(), []int{62} } func (x *ListChangesByStatusResponse) GetChanges() []*Change { @@ -4624,7 +4747,7 @@ type CreateChangeRequest struct { func (x *CreateChangeRequest) Reset() { *x = CreateChangeRequest{} - mi := &file_changes_proto_msgTypes[62] + mi := &file_changes_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4636,7 +4759,7 @@ func (x *CreateChangeRequest) String() string { func (*CreateChangeRequest) ProtoMessage() {} func (x *CreateChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[62] + mi := &file_changes_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4649,7 +4772,7 @@ func (x *CreateChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChangeRequest.ProtoReflect.Descriptor instead. func (*CreateChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{62} + return file_changes_proto_rawDescGZIP(), []int{63} } func (x *CreateChangeRequest) GetProperties() *ChangeProperties { @@ -4668,7 +4791,7 @@ type CreateChangeResponse struct { func (x *CreateChangeResponse) Reset() { *x = CreateChangeResponse{} - mi := &file_changes_proto_msgTypes[63] + mi := &file_changes_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4680,7 +4803,7 @@ func (x *CreateChangeResponse) String() string { func (*CreateChangeResponse) ProtoMessage() {} func (x *CreateChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[63] + mi := &file_changes_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4693,7 +4816,7 @@ func (x *CreateChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChangeResponse.ProtoReflect.Descriptor instead. func (*CreateChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{63} + return file_changes_proto_rawDescGZIP(), []int{64} } func (x *CreateChangeResponse) GetChange() *Change { @@ -4718,7 +4841,7 @@ type GetChangeRequest struct { func (x *GetChangeRequest) Reset() { *x = GetChangeRequest{} - mi := &file_changes_proto_msgTypes[64] + mi := &file_changes_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4730,7 +4853,7 @@ func (x *GetChangeRequest) String() string { func (*GetChangeRequest) ProtoMessage() {} func (x *GetChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[64] + mi := &file_changes_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4743,7 +4866,7 @@ func (x *GetChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRequest.ProtoReflect.Descriptor instead. func (*GetChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{64} + return file_changes_proto_rawDescGZIP(), []int{65} } func (x *GetChangeRequest) GetUUID() []byte { @@ -4769,7 +4892,7 @@ type GetChangeByTicketLinkRequest struct { func (x *GetChangeByTicketLinkRequest) Reset() { *x = GetChangeByTicketLinkRequest{} - mi := &file_changes_proto_msgTypes[65] + mi := &file_changes_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4781,7 +4904,7 @@ func (x *GetChangeByTicketLinkRequest) String() string { func (*GetChangeByTicketLinkRequest) ProtoMessage() {} func (x *GetChangeByTicketLinkRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[65] + mi := &file_changes_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4794,7 +4917,7 @@ func (x *GetChangeByTicketLinkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeByTicketLinkRequest.ProtoReflect.Descriptor instead. func (*GetChangeByTicketLinkRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{65} + return file_changes_proto_rawDescGZIP(), []int{66} } func (x *GetChangeByTicketLinkRequest) GetTicketLink() string { @@ -4824,7 +4947,7 @@ type GetChangeSummaryRequest struct { func (x *GetChangeSummaryRequest) Reset() { *x = GetChangeSummaryRequest{} - mi := &file_changes_proto_msgTypes[66] + mi := &file_changes_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4836,7 +4959,7 @@ func (x *GetChangeSummaryRequest) String() string { func (*GetChangeSummaryRequest) ProtoMessage() {} func (x *GetChangeSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[66] + mi := &file_changes_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4849,7 +4972,7 @@ func (x *GetChangeSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSummaryRequest.ProtoReflect.Descriptor instead. func (*GetChangeSummaryRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{66} + return file_changes_proto_rawDescGZIP(), []int{67} } func (x *GetChangeSummaryRequest) GetUUID() []byte { @@ -4896,7 +5019,7 @@ type GetChangeSummaryResponse struct { func (x *GetChangeSummaryResponse) Reset() { *x = GetChangeSummaryResponse{} - mi := &file_changes_proto_msgTypes[67] + mi := &file_changes_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4908,7 +5031,7 @@ func (x *GetChangeSummaryResponse) String() string { func (*GetChangeSummaryResponse) ProtoMessage() {} func (x *GetChangeSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[67] + mi := &file_changes_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4921,7 +5044,7 @@ func (x *GetChangeSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSummaryResponse.ProtoReflect.Descriptor instead. func (*GetChangeSummaryResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{67} + return file_changes_proto_rawDescGZIP(), []int{68} } func (x *GetChangeSummaryResponse) GetChange() string { @@ -4942,7 +5065,7 @@ type GetChangeSignalsRequest struct { func (x *GetChangeSignalsRequest) Reset() { *x = GetChangeSignalsRequest{} - mi := &file_changes_proto_msgTypes[68] + mi := &file_changes_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4954,7 +5077,7 @@ func (x *GetChangeSignalsRequest) String() string { func (*GetChangeSignalsRequest) ProtoMessage() {} func (x *GetChangeSignalsRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[68] + mi := &file_changes_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4967,7 +5090,7 @@ func (x *GetChangeSignalsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSignalsRequest.ProtoReflect.Descriptor instead. func (*GetChangeSignalsRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{68} + return file_changes_proto_rawDescGZIP(), []int{69} } func (x *GetChangeSignalsRequest) GetUUID() []byte { @@ -4993,7 +5116,7 @@ type GetChangeSignalsResponse struct { func (x *GetChangeSignalsResponse) Reset() { *x = GetChangeSignalsResponse{} - mi := &file_changes_proto_msgTypes[69] + mi := &file_changes_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5005,7 +5128,7 @@ func (x *GetChangeSignalsResponse) String() string { func (*GetChangeSignalsResponse) ProtoMessage() {} func (x *GetChangeSignalsResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[69] + mi := &file_changes_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5018,7 +5141,7 @@ func (x *GetChangeSignalsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSignalsResponse.ProtoReflect.Descriptor instead. func (*GetChangeSignalsResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{69} + return file_changes_proto_rawDescGZIP(), []int{70} } func (x *GetChangeSignalsResponse) GetSignals() string { @@ -5037,7 +5160,7 @@ type GetChangeResponse struct { func (x *GetChangeResponse) Reset() { *x = GetChangeResponse{} - mi := &file_changes_proto_msgTypes[70] + mi := &file_changes_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5049,7 +5172,7 @@ func (x *GetChangeResponse) String() string { func (*GetChangeResponse) ProtoMessage() {} func (x *GetChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[70] + mi := &file_changes_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5062,7 +5185,7 @@ func (x *GetChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeResponse.ProtoReflect.Descriptor instead. func (*GetChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{70} + return file_changes_proto_rawDescGZIP(), []int{71} } func (x *GetChangeResponse) GetChange() *Change { @@ -5082,7 +5205,7 @@ type GetChangeRisksRequest struct { func (x *GetChangeRisksRequest) Reset() { *x = GetChangeRisksRequest{} - mi := &file_changes_proto_msgTypes[71] + mi := &file_changes_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5094,7 +5217,7 @@ func (x *GetChangeRisksRequest) String() string { func (*GetChangeRisksRequest) ProtoMessage() {} func (x *GetChangeRisksRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[71] + mi := &file_changes_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5107,7 +5230,7 @@ func (x *GetChangeRisksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRisksRequest.ProtoReflect.Descriptor instead. func (*GetChangeRisksRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{71} + return file_changes_proto_rawDescGZIP(), []int{72} } func (x *GetChangeRisksRequest) GetUUID() []byte { @@ -5135,7 +5258,7 @@ type ChangeRiskMetadata struct { func (x *ChangeRiskMetadata) Reset() { *x = ChangeRiskMetadata{} - mi := &file_changes_proto_msgTypes[72] + mi := &file_changes_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5147,7 +5270,7 @@ func (x *ChangeRiskMetadata) String() string { func (*ChangeRiskMetadata) ProtoMessage() {} func (x *ChangeRiskMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[72] + mi := &file_changes_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5160,7 +5283,7 @@ func (x *ChangeRiskMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeRiskMetadata.ProtoReflect.Descriptor instead. func (*ChangeRiskMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{72} + return file_changes_proto_rawDescGZIP(), []int{73} } func (x *ChangeRiskMetadata) GetChangeAnalysisStatus() *ChangeAnalysisStatus { @@ -5207,7 +5330,7 @@ type GetChangeRisksResponse struct { func (x *GetChangeRisksResponse) Reset() { *x = GetChangeRisksResponse{} - mi := &file_changes_proto_msgTypes[73] + mi := &file_changes_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5219,7 +5342,7 @@ func (x *GetChangeRisksResponse) String() string { func (*GetChangeRisksResponse) ProtoMessage() {} func (x *GetChangeRisksResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[73] + mi := &file_changes_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5232,7 +5355,7 @@ func (x *GetChangeRisksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRisksResponse.ProtoReflect.Descriptor instead. func (*GetChangeRisksResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{73} + return file_changes_proto_rawDescGZIP(), []int{74} } func (x *GetChangeRisksResponse) GetChangeRiskMetadata() *ChangeRiskMetadata { @@ -5253,7 +5376,7 @@ type UpdateChangeRequest struct { func (x *UpdateChangeRequest) Reset() { *x = UpdateChangeRequest{} - mi := &file_changes_proto_msgTypes[74] + mi := &file_changes_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5265,7 +5388,7 @@ func (x *UpdateChangeRequest) String() string { func (*UpdateChangeRequest) ProtoMessage() {} func (x *UpdateChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[74] + mi := &file_changes_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5278,7 +5401,7 @@ func (x *UpdateChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChangeRequest.ProtoReflect.Descriptor instead. func (*UpdateChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{74} + return file_changes_proto_rawDescGZIP(), []int{75} } func (x *UpdateChangeRequest) GetUUID() []byte { @@ -5304,7 +5427,7 @@ type UpdateChangeResponse struct { func (x *UpdateChangeResponse) Reset() { *x = UpdateChangeResponse{} - mi := &file_changes_proto_msgTypes[75] + mi := &file_changes_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5316,7 +5439,7 @@ func (x *UpdateChangeResponse) String() string { func (*UpdateChangeResponse) ProtoMessage() {} func (x *UpdateChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[75] + mi := &file_changes_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5329,7 +5452,7 @@ func (x *UpdateChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChangeResponse.ProtoReflect.Descriptor instead. func (*UpdateChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{75} + return file_changes_proto_rawDescGZIP(), []int{76} } func (x *UpdateChangeResponse) GetChange() *Change { @@ -5349,7 +5472,7 @@ type DeleteChangeRequest struct { func (x *DeleteChangeRequest) Reset() { *x = DeleteChangeRequest{} - mi := &file_changes_proto_msgTypes[76] + mi := &file_changes_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5361,7 +5484,7 @@ func (x *DeleteChangeRequest) String() string { func (*DeleteChangeRequest) ProtoMessage() {} func (x *DeleteChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[76] + mi := &file_changes_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5374,7 +5497,7 @@ func (x *DeleteChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteChangeRequest.ProtoReflect.Descriptor instead. func (*DeleteChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{76} + return file_changes_proto_rawDescGZIP(), []int{77} } func (x *DeleteChangeRequest) GetUUID() []byte { @@ -5394,7 +5517,7 @@ type ListChangesBySnapshotUUIDRequest struct { func (x *ListChangesBySnapshotUUIDRequest) Reset() { *x = ListChangesBySnapshotUUIDRequest{} - mi := &file_changes_proto_msgTypes[77] + mi := &file_changes_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5406,7 +5529,7 @@ func (x *ListChangesBySnapshotUUIDRequest) String() string { func (*ListChangesBySnapshotUUIDRequest) ProtoMessage() {} func (x *ListChangesBySnapshotUUIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[77] + mi := &file_changes_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5419,7 +5542,7 @@ func (x *ListChangesBySnapshotUUIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesBySnapshotUUIDRequest.ProtoReflect.Descriptor instead. func (*ListChangesBySnapshotUUIDRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{77} + return file_changes_proto_rawDescGZIP(), []int{78} } func (x *ListChangesBySnapshotUUIDRequest) GetUUID() []byte { @@ -5438,7 +5561,7 @@ type ListChangesBySnapshotUUIDResponse struct { func (x *ListChangesBySnapshotUUIDResponse) Reset() { *x = ListChangesBySnapshotUUIDResponse{} - mi := &file_changes_proto_msgTypes[78] + mi := &file_changes_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5450,7 +5573,7 @@ func (x *ListChangesBySnapshotUUIDResponse) String() string { func (*ListChangesBySnapshotUUIDResponse) ProtoMessage() {} func (x *ListChangesBySnapshotUUIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[78] + mi := &file_changes_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5463,7 +5586,7 @@ func (x *ListChangesBySnapshotUUIDResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListChangesBySnapshotUUIDResponse.ProtoReflect.Descriptor instead. func (*ListChangesBySnapshotUUIDResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{78} + return file_changes_proto_rawDescGZIP(), []int{79} } func (x *ListChangesBySnapshotUUIDResponse) GetChanges() []*Change { @@ -5481,7 +5604,7 @@ type DeleteChangeResponse struct { func (x *DeleteChangeResponse) Reset() { *x = DeleteChangeResponse{} - mi := &file_changes_proto_msgTypes[79] + mi := &file_changes_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5493,7 +5616,7 @@ func (x *DeleteChangeResponse) String() string { func (*DeleteChangeResponse) ProtoMessage() {} func (x *DeleteChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[79] + mi := &file_changes_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5506,7 +5629,7 @@ func (x *DeleteChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteChangeResponse.ProtoReflect.Descriptor instead. func (*DeleteChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{79} + return file_changes_proto_rawDescGZIP(), []int{80} } type RefreshStateRequest struct { @@ -5517,7 +5640,7 @@ type RefreshStateRequest struct { func (x *RefreshStateRequest) Reset() { *x = RefreshStateRequest{} - mi := &file_changes_proto_msgTypes[80] + mi := &file_changes_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5529,7 +5652,7 @@ func (x *RefreshStateRequest) String() string { func (*RefreshStateRequest) ProtoMessage() {} func (x *RefreshStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[80] + mi := &file_changes_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5542,7 +5665,7 @@ func (x *RefreshStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshStateRequest.ProtoReflect.Descriptor instead. func (*RefreshStateRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{80} + return file_changes_proto_rawDescGZIP(), []int{81} } type RefreshStateResponse struct { @@ -5553,7 +5676,7 @@ type RefreshStateResponse struct { func (x *RefreshStateResponse) Reset() { *x = RefreshStateResponse{} - mi := &file_changes_proto_msgTypes[81] + mi := &file_changes_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5565,7 +5688,7 @@ func (x *RefreshStateResponse) String() string { func (*RefreshStateResponse) ProtoMessage() {} func (x *RefreshStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[81] + mi := &file_changes_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5578,7 +5701,7 @@ func (x *RefreshStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshStateResponse.ProtoReflect.Descriptor instead. func (*RefreshStateResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{81} + return file_changes_proto_rawDescGZIP(), []int{82} } type StartChangeRequest struct { @@ -5590,7 +5713,7 @@ type StartChangeRequest struct { func (x *StartChangeRequest) Reset() { *x = StartChangeRequest{} - mi := &file_changes_proto_msgTypes[82] + mi := &file_changes_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5602,7 +5725,7 @@ func (x *StartChangeRequest) String() string { func (*StartChangeRequest) ProtoMessage() {} func (x *StartChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[82] + mi := &file_changes_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5615,7 +5738,7 @@ func (x *StartChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeRequest.ProtoReflect.Descriptor instead. func (*StartChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{82} + return file_changes_proto_rawDescGZIP(), []int{83} } func (x *StartChangeRequest) GetChangeUUID() []byte { @@ -5636,7 +5759,7 @@ type StartChangeResponse struct { func (x *StartChangeResponse) Reset() { *x = StartChangeResponse{} - mi := &file_changes_proto_msgTypes[83] + mi := &file_changes_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5648,7 +5771,7 @@ func (x *StartChangeResponse) String() string { func (*StartChangeResponse) ProtoMessage() {} func (x *StartChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[83] + mi := &file_changes_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5661,7 +5784,7 @@ func (x *StartChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeResponse.ProtoReflect.Descriptor instead. func (*StartChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{83} + return file_changes_proto_rawDescGZIP(), []int{84} } func (x *StartChangeResponse) GetState() StartChangeResponse_State { @@ -5694,7 +5817,7 @@ type EndChangeRequest struct { func (x *EndChangeRequest) Reset() { *x = EndChangeRequest{} - mi := &file_changes_proto_msgTypes[84] + mi := &file_changes_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5706,7 +5829,7 @@ func (x *EndChangeRequest) String() string { func (*EndChangeRequest) ProtoMessage() {} func (x *EndChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[84] + mi := &file_changes_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5719,7 +5842,7 @@ func (x *EndChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EndChangeRequest.ProtoReflect.Descriptor instead. func (*EndChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{84} + return file_changes_proto_rawDescGZIP(), []int{85} } func (x *EndChangeRequest) GetChangeUUID() []byte { @@ -5740,7 +5863,7 @@ type EndChangeResponse struct { func (x *EndChangeResponse) Reset() { *x = EndChangeResponse{} - mi := &file_changes_proto_msgTypes[85] + mi := &file_changes_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5752,7 +5875,7 @@ func (x *EndChangeResponse) String() string { func (*EndChangeResponse) ProtoMessage() {} func (x *EndChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[85] + mi := &file_changes_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5765,7 +5888,7 @@ func (x *EndChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EndChangeResponse.ProtoReflect.Descriptor instead. func (*EndChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{85} + return file_changes_proto_rawDescGZIP(), []int{86} } func (x *EndChangeResponse) GetState() EndChangeResponse_State { @@ -5802,7 +5925,7 @@ type Risk struct { func (x *Risk) Reset() { *x = Risk{} - mi := &file_changes_proto_msgTypes[86] + mi := &file_changes_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5814,7 +5937,7 @@ func (x *Risk) String() string { func (*Risk) ProtoMessage() {} func (x *Risk) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[86] + mi := &file_changes_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5827,7 +5950,7 @@ func (x *Risk) ProtoReflect() protoreflect.Message { // Deprecated: Use Risk.ProtoReflect.Descriptor instead. func (*Risk) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{86} + return file_changes_proto_rawDescGZIP(), []int{87} } func (x *Risk) GetUUID() []byte { @@ -5874,7 +5997,7 @@ type ChangeAnalysisStatus struct { func (x *ChangeAnalysisStatus) Reset() { *x = ChangeAnalysisStatus{} - mi := &file_changes_proto_msgTypes[87] + mi := &file_changes_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5886,7 +6009,7 @@ func (x *ChangeAnalysisStatus) String() string { func (*ChangeAnalysisStatus) ProtoMessage() {} func (x *ChangeAnalysisStatus) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[87] + mi := &file_changes_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5899,7 +6022,7 @@ func (x *ChangeAnalysisStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeAnalysisStatus.ProtoReflect.Descriptor instead. func (*ChangeAnalysisStatus) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{87} + return file_changes_proto_rawDescGZIP(), []int{88} } func (x *ChangeAnalysisStatus) GetStatus() ChangeAnalysisStatus_Status { @@ -5920,7 +6043,7 @@ type GenerateRiskFixRequest struct { func (x *GenerateRiskFixRequest) Reset() { *x = GenerateRiskFixRequest{} - mi := &file_changes_proto_msgTypes[88] + mi := &file_changes_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5932,7 +6055,7 @@ func (x *GenerateRiskFixRequest) String() string { func (*GenerateRiskFixRequest) ProtoMessage() {} func (x *GenerateRiskFixRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[88] + mi := &file_changes_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5945,7 +6068,7 @@ func (x *GenerateRiskFixRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateRiskFixRequest.ProtoReflect.Descriptor instead. func (*GenerateRiskFixRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{88} + return file_changes_proto_rawDescGZIP(), []int{89} } func (x *GenerateRiskFixRequest) GetRiskUUID() []byte { @@ -5965,7 +6088,7 @@ type GenerateRiskFixResponse struct { func (x *GenerateRiskFixResponse) Reset() { *x = GenerateRiskFixResponse{} - mi := &file_changes_proto_msgTypes[89] + mi := &file_changes_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5977,7 +6100,7 @@ func (x *GenerateRiskFixResponse) String() string { func (*GenerateRiskFixResponse) ProtoMessage() {} func (x *GenerateRiskFixResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[89] + mi := &file_changes_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5990,7 +6113,7 @@ func (x *GenerateRiskFixResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateRiskFixResponse.ProtoReflect.Descriptor instead. func (*GenerateRiskFixResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{89} + return file_changes_proto_rawDescGZIP(), []int{90} } func (x *GenerateRiskFixResponse) GetFixSuggestion() string { @@ -6020,7 +6143,7 @@ type ChangeMetadata_HealthChange struct { func (x *ChangeMetadata_HealthChange) Reset() { *x = ChangeMetadata_HealthChange{} - mi := &file_changes_proto_msgTypes[92] + mi := &file_changes_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6032,7 +6155,7 @@ func (x *ChangeMetadata_HealthChange) String() string { func (*ChangeMetadata_HealthChange) ProtoMessage() {} func (x *ChangeMetadata_HealthChange) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[92] + mi := &file_changes_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6045,7 +6168,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{55, 0} + return file_changes_proto_rawDescGZIP(), []int{56, 0} } func (x *ChangeMetadata_HealthChange) GetAdded() int32 { @@ -6175,9 +6298,15 @@ const file_changes_proto_rawDesc = "" + "\n" + "\b_endedAtB\b\n" + "\x06_actor\"\x0e\n" + - "\fEmptyContent\"U\n" + - "\x18MappedItemsTimelineEntry\x129\n" + - "\vmappedItems\x18\x01 \x03(\v2\x17.changes.MappedItemDiffR\vmappedItems\"b\n" + + "\fEmptyContent\"\xb5\x01\n" + + "\x19MappedItemTimelineSummary\x12!\n" + + "\fdisplay_name\x18\x01 \x01(\tR\vdisplayName\x129\n" + + "\x06status\x18\x02 \x01(\x0e2!.changes.MappedItemTimelineStatusR\x06status\x12(\n" + + "\rerror_message\x18\x03 \x01(\tH\x00R\ferrorMessage\x88\x01\x01B\x10\n" + + "\x0e_error_message\"\x93\x01\n" + + "\x18MappedItemsTimelineEntry\x12=\n" + + "\vmappedItems\x18\x01 \x03(\v2\x17.changes.MappedItemDiffB\x02\x18\x01R\vmappedItems\x128\n" + + "\x05items\x18\x02 \x03(\v2\".changes.MappedItemTimelineSummaryR\x05items\"b\n" + "\"CalculatedBlastRadiusTimelineEntry\x12\x1a\n" + "\bnumItems\x18\x01 \x01(\rR\bnumItems\x12\x1a\n" + "\bnumEdges\x18\x02 \x01(\rR\bnumEdgesJ\x04\b\x04\x10\x05\"K\n" + @@ -6505,7 +6634,12 @@ const file_changes_proto_rawDesc = "" + "\x16GenerateRiskFixRequest\x12\x1a\n" + "\briskUUID\x18\x01 \x01(\fR\briskUUID\"?\n" + "\x17GenerateRiskFixResponse\x12$\n" + - "\rfixSuggestion\x18\x01 \x01(\tR\rfixSuggestion*\xf9\x01\n" + + "\rfixSuggestion\x18\x01 \x01(\tR\rfixSuggestion*\xc4\x01\n" + + "\x18MappedItemTimelineStatus\x12+\n" + + "'MAPPED_ITEM_TIMELINE_STATUS_UNSPECIFIED\x10\x00\x12'\n" + + "#MAPPED_ITEM_TIMELINE_STATUS_SUCCESS\x10\x01\x12%\n" + + "!MAPPED_ITEM_TIMELINE_STATUS_ERROR\x10\x02\x12+\n" + + "'MAPPED_ITEM_TIMELINE_STATUS_UNSUPPORTED\x10\x03*\xf9\x01\n" + "\x10HypothesisStatus\x12.\n" + "*INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED\x10\x00\x12*\n" + "&INVESTIGATED_HYPOTHESIS_STATUS_FORMING\x10\x01\x120\n" + @@ -6583,299 +6717,303 @@ func file_changes_proto_rawDescGZIP() []byte { return file_changes_proto_rawDescData } -var file_changes_proto_enumTypes = make([]protoimpl.EnumInfo, 10) -var file_changes_proto_msgTypes = make([]protoimpl.MessageInfo, 94) +var file_changes_proto_enumTypes = make([]protoimpl.EnumInfo, 11) +var file_changes_proto_msgTypes = make([]protoimpl.MessageInfo, 95) var file_changes_proto_goTypes = []any{ - (HypothesisStatus)(0), // 0: changes.HypothesisStatus - (ChangeTimelineEntryStatus)(0), // 1: changes.ChangeTimelineEntryStatus - (ItemDiffStatus)(0), // 2: changes.ItemDiffStatus - (ChangeOutputFormat)(0), // 3: changes.ChangeOutputFormat - (LabelType)(0), // 4: changes.LabelType - (ChangeStatus)(0), // 5: changes.ChangeStatus - (StartChangeResponse_State)(0), // 6: changes.StartChangeResponse.State - (EndChangeResponse_State)(0), // 7: changes.EndChangeResponse.State - (Risk_Severity)(0), // 8: changes.Risk.Severity - (ChangeAnalysisStatus_Status)(0), // 9: changes.ChangeAnalysisStatus.Status - (*LabelRule)(nil), // 10: changes.LabelRule - (*LabelRuleMetadata)(nil), // 11: changes.LabelRuleMetadata - (*LabelRuleProperties)(nil), // 12: changes.LabelRuleProperties - (*ListLabelRulesRequest)(nil), // 13: changes.ListLabelRulesRequest - (*ListLabelRulesResponse)(nil), // 14: changes.ListLabelRulesResponse - (*CreateLabelRuleRequest)(nil), // 15: changes.CreateLabelRuleRequest - (*CreateLabelRuleResponse)(nil), // 16: changes.CreateLabelRuleResponse - (*GetLabelRuleRequest)(nil), // 17: changes.GetLabelRuleRequest - (*GetLabelRuleResponse)(nil), // 18: changes.GetLabelRuleResponse - (*UpdateLabelRuleRequest)(nil), // 19: changes.UpdateLabelRuleRequest - (*UpdateLabelRuleResponse)(nil), // 20: changes.UpdateLabelRuleResponse - (*DeleteLabelRuleRequest)(nil), // 21: changes.DeleteLabelRuleRequest - (*DeleteLabelRuleResponse)(nil), // 22: changes.DeleteLabelRuleResponse - (*TestLabelRuleRequest)(nil), // 23: changes.TestLabelRuleRequest - (*TestLabelRuleResponse)(nil), // 24: changes.TestLabelRuleResponse - (*ReapplyLabelRuleInTimeRangeRequest)(nil), // 25: changes.ReapplyLabelRuleInTimeRangeRequest - (*ReapplyLabelRuleInTimeRangeResponse)(nil), // 26: changes.ReapplyLabelRuleInTimeRangeResponse - (*GetHypothesesDetailsRequest)(nil), // 27: changes.GetHypothesesDetailsRequest - (*GetHypothesesDetailsResponse)(nil), // 28: changes.GetHypothesesDetailsResponse - (*HypothesesDetails)(nil), // 29: changes.HypothesesDetails - (*GetChangeTimelineV2Request)(nil), // 30: changes.GetChangeTimelineV2Request - (*GetChangeTimelineV2Response)(nil), // 31: changes.GetChangeTimelineV2Response - (*ChangeTimelineEntryV2)(nil), // 32: changes.ChangeTimelineEntryV2 - (*EmptyContent)(nil), // 33: changes.EmptyContent - (*MappedItemsTimelineEntry)(nil), // 34: changes.MappedItemsTimelineEntry - (*CalculatedBlastRadiusTimelineEntry)(nil), // 35: changes.CalculatedBlastRadiusTimelineEntry - (*RecordObservationsTimelineEntry)(nil), // 36: changes.RecordObservationsTimelineEntry - (*FormHypothesesTimelineEntry)(nil), // 37: changes.FormHypothesesTimelineEntry - (*InvestigateHypothesesTimelineEntry)(nil), // 38: changes.InvestigateHypothesesTimelineEntry - (*HypothesisSummary)(nil), // 39: changes.HypothesisSummary - (*CalculatedRisksTimelineEntry)(nil), // 40: changes.CalculatedRisksTimelineEntry - (*CalculatedLabelsTimelineEntry)(nil), // 41: changes.CalculatedLabelsTimelineEntry - (*ChangeValidationTimelineEntry)(nil), // 42: changes.ChangeValidationTimelineEntry - (*ChangeValidationCategory)(nil), // 43: changes.ChangeValidationCategory - (*GetDiffRequest)(nil), // 44: changes.GetDiffRequest - (*GetDiffResponse)(nil), // 45: changes.GetDiffResponse - (*ListChangingItemsSummaryRequest)(nil), // 46: changes.ListChangingItemsSummaryRequest - (*ListChangingItemsSummaryResponse)(nil), // 47: changes.ListChangingItemsSummaryResponse - (*MappedItemDiff)(nil), // 48: changes.MappedItemDiff - (*StartChangeAnalysisRequest)(nil), // 49: changes.StartChangeAnalysisRequest - (*StartChangeAnalysisResponse)(nil), // 50: changes.StartChangeAnalysisResponse - (*ListHomeChangesRequest)(nil), // 51: changes.ListHomeChangesRequest - (*ChangeFiltersRequest)(nil), // 52: changes.ChangeFiltersRequest - (*ListHomeChangesResponse)(nil), // 53: changes.ListHomeChangesResponse - (*PopulateChangeFiltersRequest)(nil), // 54: changes.PopulateChangeFiltersRequest - (*PopulateChangeFiltersResponse)(nil), // 55: changes.PopulateChangeFiltersResponse - (*ItemDiffSummary)(nil), // 56: changes.ItemDiffSummary - (*ItemDiff)(nil), // 57: changes.ItemDiff - (*EnrichedTags)(nil), // 58: changes.EnrichedTags - (*TagValue)(nil), // 59: changes.TagValue - (*UserTagValue)(nil), // 60: changes.UserTagValue - (*AutoTagValue)(nil), // 61: changes.AutoTagValue - (*Label)(nil), // 62: changes.Label - (*ChangeSummary)(nil), // 63: changes.ChangeSummary - (*Change)(nil), // 64: changes.Change - (*ChangeMetadata)(nil), // 65: changes.ChangeMetadata - (*ChangeProperties)(nil), // 66: changes.ChangeProperties - (*GithubChangeInfo)(nil), // 67: changes.GithubChangeInfo - (*ListChangesRequest)(nil), // 68: changes.ListChangesRequest - (*ListChangesResponse)(nil), // 69: changes.ListChangesResponse - (*ListChangesByStatusRequest)(nil), // 70: changes.ListChangesByStatusRequest - (*ListChangesByStatusResponse)(nil), // 71: changes.ListChangesByStatusResponse - (*CreateChangeRequest)(nil), // 72: changes.CreateChangeRequest - (*CreateChangeResponse)(nil), // 73: changes.CreateChangeResponse - (*GetChangeRequest)(nil), // 74: changes.GetChangeRequest - (*GetChangeByTicketLinkRequest)(nil), // 75: changes.GetChangeByTicketLinkRequest - (*GetChangeSummaryRequest)(nil), // 76: changes.GetChangeSummaryRequest - (*GetChangeSummaryResponse)(nil), // 77: changes.GetChangeSummaryResponse - (*GetChangeSignalsRequest)(nil), // 78: changes.GetChangeSignalsRequest - (*GetChangeSignalsResponse)(nil), // 79: changes.GetChangeSignalsResponse - (*GetChangeResponse)(nil), // 80: changes.GetChangeResponse - (*GetChangeRisksRequest)(nil), // 81: changes.GetChangeRisksRequest - (*ChangeRiskMetadata)(nil), // 82: changes.ChangeRiskMetadata - (*GetChangeRisksResponse)(nil), // 83: changes.GetChangeRisksResponse - (*UpdateChangeRequest)(nil), // 84: changes.UpdateChangeRequest - (*UpdateChangeResponse)(nil), // 85: changes.UpdateChangeResponse - (*DeleteChangeRequest)(nil), // 86: changes.DeleteChangeRequest - (*ListChangesBySnapshotUUIDRequest)(nil), // 87: changes.ListChangesBySnapshotUUIDRequest - (*ListChangesBySnapshotUUIDResponse)(nil), // 88: changes.ListChangesBySnapshotUUIDResponse - (*DeleteChangeResponse)(nil), // 89: changes.DeleteChangeResponse - (*RefreshStateRequest)(nil), // 90: changes.RefreshStateRequest - (*RefreshStateResponse)(nil), // 91: changes.RefreshStateResponse - (*StartChangeRequest)(nil), // 92: changes.StartChangeRequest - (*StartChangeResponse)(nil), // 93: changes.StartChangeResponse - (*EndChangeRequest)(nil), // 94: changes.EndChangeRequest - (*EndChangeResponse)(nil), // 95: changes.EndChangeResponse - (*Risk)(nil), // 96: changes.Risk - (*ChangeAnalysisStatus)(nil), // 97: changes.ChangeAnalysisStatus - (*GenerateRiskFixRequest)(nil), // 98: changes.GenerateRiskFixRequest - (*GenerateRiskFixResponse)(nil), // 99: changes.GenerateRiskFixResponse - nil, // 100: changes.EnrichedTags.TagValueEntry - nil, // 101: changes.ChangeSummary.TagsEntry - (*ChangeMetadata_HealthChange)(nil), // 102: changes.ChangeMetadata.HealthChange - nil, // 103: changes.ChangeProperties.TagsEntry - (*timestamppb.Timestamp)(nil), // 104: google.protobuf.Timestamp - (*Edge)(nil), // 105: Edge - (*Query)(nil), // 106: Query - (*QueryError)(nil), // 107: QueryError - (*BlastRadiusConfig)(nil), // 108: config.BlastRadiusConfig - (*RoutineChangesConfig)(nil), // 109: config.RoutineChangesConfig - (*GithubOrganisationProfile)(nil), // 110: config.GithubOrganisationProfile - (*PaginationRequest)(nil), // 111: PaginationRequest - (SortOrder)(0), // 112: SortOrder - (*PaginationResponse)(nil), // 113: PaginationResponse - (*Reference)(nil), // 114: Reference - (Health)(0), // 115: Health - (*Item)(nil), // 116: Item + (MappedItemTimelineStatus)(0), // 0: changes.MappedItemTimelineStatus + (HypothesisStatus)(0), // 1: changes.HypothesisStatus + (ChangeTimelineEntryStatus)(0), // 2: changes.ChangeTimelineEntryStatus + (ItemDiffStatus)(0), // 3: changes.ItemDiffStatus + (ChangeOutputFormat)(0), // 4: changes.ChangeOutputFormat + (LabelType)(0), // 5: changes.LabelType + (ChangeStatus)(0), // 6: changes.ChangeStatus + (StartChangeResponse_State)(0), // 7: changes.StartChangeResponse.State + (EndChangeResponse_State)(0), // 8: changes.EndChangeResponse.State + (Risk_Severity)(0), // 9: changes.Risk.Severity + (ChangeAnalysisStatus_Status)(0), // 10: changes.ChangeAnalysisStatus.Status + (*LabelRule)(nil), // 11: changes.LabelRule + (*LabelRuleMetadata)(nil), // 12: changes.LabelRuleMetadata + (*LabelRuleProperties)(nil), // 13: changes.LabelRuleProperties + (*ListLabelRulesRequest)(nil), // 14: changes.ListLabelRulesRequest + (*ListLabelRulesResponse)(nil), // 15: changes.ListLabelRulesResponse + (*CreateLabelRuleRequest)(nil), // 16: changes.CreateLabelRuleRequest + (*CreateLabelRuleResponse)(nil), // 17: changes.CreateLabelRuleResponse + (*GetLabelRuleRequest)(nil), // 18: changes.GetLabelRuleRequest + (*GetLabelRuleResponse)(nil), // 19: changes.GetLabelRuleResponse + (*UpdateLabelRuleRequest)(nil), // 20: changes.UpdateLabelRuleRequest + (*UpdateLabelRuleResponse)(nil), // 21: changes.UpdateLabelRuleResponse + (*DeleteLabelRuleRequest)(nil), // 22: changes.DeleteLabelRuleRequest + (*DeleteLabelRuleResponse)(nil), // 23: changes.DeleteLabelRuleResponse + (*TestLabelRuleRequest)(nil), // 24: changes.TestLabelRuleRequest + (*TestLabelRuleResponse)(nil), // 25: changes.TestLabelRuleResponse + (*ReapplyLabelRuleInTimeRangeRequest)(nil), // 26: changes.ReapplyLabelRuleInTimeRangeRequest + (*ReapplyLabelRuleInTimeRangeResponse)(nil), // 27: changes.ReapplyLabelRuleInTimeRangeResponse + (*GetHypothesesDetailsRequest)(nil), // 28: changes.GetHypothesesDetailsRequest + (*GetHypothesesDetailsResponse)(nil), // 29: changes.GetHypothesesDetailsResponse + (*HypothesesDetails)(nil), // 30: changes.HypothesesDetails + (*GetChangeTimelineV2Request)(nil), // 31: changes.GetChangeTimelineV2Request + (*GetChangeTimelineV2Response)(nil), // 32: changes.GetChangeTimelineV2Response + (*ChangeTimelineEntryV2)(nil), // 33: changes.ChangeTimelineEntryV2 + (*EmptyContent)(nil), // 34: changes.EmptyContent + (*MappedItemTimelineSummary)(nil), // 35: changes.MappedItemTimelineSummary + (*MappedItemsTimelineEntry)(nil), // 36: changes.MappedItemsTimelineEntry + (*CalculatedBlastRadiusTimelineEntry)(nil), // 37: changes.CalculatedBlastRadiusTimelineEntry + (*RecordObservationsTimelineEntry)(nil), // 38: changes.RecordObservationsTimelineEntry + (*FormHypothesesTimelineEntry)(nil), // 39: changes.FormHypothesesTimelineEntry + (*InvestigateHypothesesTimelineEntry)(nil), // 40: changes.InvestigateHypothesesTimelineEntry + (*HypothesisSummary)(nil), // 41: changes.HypothesisSummary + (*CalculatedRisksTimelineEntry)(nil), // 42: changes.CalculatedRisksTimelineEntry + (*CalculatedLabelsTimelineEntry)(nil), // 43: changes.CalculatedLabelsTimelineEntry + (*ChangeValidationTimelineEntry)(nil), // 44: changes.ChangeValidationTimelineEntry + (*ChangeValidationCategory)(nil), // 45: changes.ChangeValidationCategory + (*GetDiffRequest)(nil), // 46: changes.GetDiffRequest + (*GetDiffResponse)(nil), // 47: changes.GetDiffResponse + (*ListChangingItemsSummaryRequest)(nil), // 48: changes.ListChangingItemsSummaryRequest + (*ListChangingItemsSummaryResponse)(nil), // 49: changes.ListChangingItemsSummaryResponse + (*MappedItemDiff)(nil), // 50: changes.MappedItemDiff + (*StartChangeAnalysisRequest)(nil), // 51: changes.StartChangeAnalysisRequest + (*StartChangeAnalysisResponse)(nil), // 52: changes.StartChangeAnalysisResponse + (*ListHomeChangesRequest)(nil), // 53: changes.ListHomeChangesRequest + (*ChangeFiltersRequest)(nil), // 54: changes.ChangeFiltersRequest + (*ListHomeChangesResponse)(nil), // 55: changes.ListHomeChangesResponse + (*PopulateChangeFiltersRequest)(nil), // 56: changes.PopulateChangeFiltersRequest + (*PopulateChangeFiltersResponse)(nil), // 57: changes.PopulateChangeFiltersResponse + (*ItemDiffSummary)(nil), // 58: changes.ItemDiffSummary + (*ItemDiff)(nil), // 59: changes.ItemDiff + (*EnrichedTags)(nil), // 60: changes.EnrichedTags + (*TagValue)(nil), // 61: changes.TagValue + (*UserTagValue)(nil), // 62: changes.UserTagValue + (*AutoTagValue)(nil), // 63: changes.AutoTagValue + (*Label)(nil), // 64: changes.Label + (*ChangeSummary)(nil), // 65: changes.ChangeSummary + (*Change)(nil), // 66: changes.Change + (*ChangeMetadata)(nil), // 67: changes.ChangeMetadata + (*ChangeProperties)(nil), // 68: changes.ChangeProperties + (*GithubChangeInfo)(nil), // 69: changes.GithubChangeInfo + (*ListChangesRequest)(nil), // 70: changes.ListChangesRequest + (*ListChangesResponse)(nil), // 71: changes.ListChangesResponse + (*ListChangesByStatusRequest)(nil), // 72: changes.ListChangesByStatusRequest + (*ListChangesByStatusResponse)(nil), // 73: changes.ListChangesByStatusResponse + (*CreateChangeRequest)(nil), // 74: changes.CreateChangeRequest + (*CreateChangeResponse)(nil), // 75: changes.CreateChangeResponse + (*GetChangeRequest)(nil), // 76: changes.GetChangeRequest + (*GetChangeByTicketLinkRequest)(nil), // 77: changes.GetChangeByTicketLinkRequest + (*GetChangeSummaryRequest)(nil), // 78: changes.GetChangeSummaryRequest + (*GetChangeSummaryResponse)(nil), // 79: changes.GetChangeSummaryResponse + (*GetChangeSignalsRequest)(nil), // 80: changes.GetChangeSignalsRequest + (*GetChangeSignalsResponse)(nil), // 81: changes.GetChangeSignalsResponse + (*GetChangeResponse)(nil), // 82: changes.GetChangeResponse + (*GetChangeRisksRequest)(nil), // 83: changes.GetChangeRisksRequest + (*ChangeRiskMetadata)(nil), // 84: changes.ChangeRiskMetadata + (*GetChangeRisksResponse)(nil), // 85: changes.GetChangeRisksResponse + (*UpdateChangeRequest)(nil), // 86: changes.UpdateChangeRequest + (*UpdateChangeResponse)(nil), // 87: changes.UpdateChangeResponse + (*DeleteChangeRequest)(nil), // 88: changes.DeleteChangeRequest + (*ListChangesBySnapshotUUIDRequest)(nil), // 89: changes.ListChangesBySnapshotUUIDRequest + (*ListChangesBySnapshotUUIDResponse)(nil), // 90: changes.ListChangesBySnapshotUUIDResponse + (*DeleteChangeResponse)(nil), // 91: changes.DeleteChangeResponse + (*RefreshStateRequest)(nil), // 92: changes.RefreshStateRequest + (*RefreshStateResponse)(nil), // 93: changes.RefreshStateResponse + (*StartChangeRequest)(nil), // 94: changes.StartChangeRequest + (*StartChangeResponse)(nil), // 95: changes.StartChangeResponse + (*EndChangeRequest)(nil), // 96: changes.EndChangeRequest + (*EndChangeResponse)(nil), // 97: changes.EndChangeResponse + (*Risk)(nil), // 98: changes.Risk + (*ChangeAnalysisStatus)(nil), // 99: changes.ChangeAnalysisStatus + (*GenerateRiskFixRequest)(nil), // 100: changes.GenerateRiskFixRequest + (*GenerateRiskFixResponse)(nil), // 101: changes.GenerateRiskFixResponse + nil, // 102: changes.EnrichedTags.TagValueEntry + nil, // 103: changes.ChangeSummary.TagsEntry + (*ChangeMetadata_HealthChange)(nil), // 104: changes.ChangeMetadata.HealthChange + nil, // 105: changes.ChangeProperties.TagsEntry + (*timestamppb.Timestamp)(nil), // 106: google.protobuf.Timestamp + (*Edge)(nil), // 107: Edge + (*Query)(nil), // 108: Query + (*QueryError)(nil), // 109: QueryError + (*BlastRadiusConfig)(nil), // 110: config.BlastRadiusConfig + (*RoutineChangesConfig)(nil), // 111: config.RoutineChangesConfig + (*GithubOrganisationProfile)(nil), // 112: config.GithubOrganisationProfile + (*PaginationRequest)(nil), // 113: PaginationRequest + (SortOrder)(0), // 114: SortOrder + (*PaginationResponse)(nil), // 115: PaginationResponse + (*Reference)(nil), // 116: Reference + (Health)(0), // 117: Health + (*Item)(nil), // 118: Item } var file_changes_proto_depIdxs = []int32{ - 11, // 0: changes.LabelRule.metadata:type_name -> changes.LabelRuleMetadata - 12, // 1: changes.LabelRule.properties:type_name -> changes.LabelRuleProperties - 104, // 2: changes.LabelRuleMetadata.createdAt:type_name -> google.protobuf.Timestamp - 104, // 3: changes.LabelRuleMetadata.updatedAt:type_name -> google.protobuf.Timestamp - 10, // 4: changes.ListLabelRulesResponse.rules:type_name -> changes.LabelRule - 12, // 5: changes.CreateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties - 10, // 6: changes.CreateLabelRuleResponse.rule:type_name -> changes.LabelRule - 10, // 7: changes.GetLabelRuleResponse.rule:type_name -> changes.LabelRule - 12, // 8: changes.UpdateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties - 10, // 9: changes.UpdateLabelRuleResponse.rule:type_name -> changes.LabelRule - 12, // 10: changes.TestLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties - 62, // 11: changes.TestLabelRuleResponse.label:type_name -> changes.Label - 104, // 12: changes.ReapplyLabelRuleInTimeRangeRequest.startAt:type_name -> google.protobuf.Timestamp - 104, // 13: changes.ReapplyLabelRuleInTimeRangeRequest.endAt:type_name -> google.protobuf.Timestamp - 29, // 14: changes.GetHypothesesDetailsResponse.hypotheses:type_name -> changes.HypothesesDetails - 0, // 15: changes.HypothesesDetails.status:type_name -> changes.HypothesisStatus - 32, // 16: changes.GetChangeTimelineV2Response.entries:type_name -> changes.ChangeTimelineEntryV2 - 1, // 17: changes.ChangeTimelineEntryV2.status:type_name -> changes.ChangeTimelineEntryStatus - 104, // 18: changes.ChangeTimelineEntryV2.startedAt:type_name -> google.protobuf.Timestamp - 104, // 19: changes.ChangeTimelineEntryV2.endedAt:type_name -> google.protobuf.Timestamp - 34, // 20: changes.ChangeTimelineEntryV2.mappedItems:type_name -> changes.MappedItemsTimelineEntry - 35, // 21: changes.ChangeTimelineEntryV2.calculatedBlastRadius:type_name -> changes.CalculatedBlastRadiusTimelineEntry - 40, // 22: changes.ChangeTimelineEntryV2.calculatedRisks:type_name -> changes.CalculatedRisksTimelineEntry - 33, // 23: changes.ChangeTimelineEntryV2.empty:type_name -> changes.EmptyContent - 42, // 24: changes.ChangeTimelineEntryV2.changeValidation:type_name -> changes.ChangeValidationTimelineEntry - 41, // 25: changes.ChangeTimelineEntryV2.calculatedLabels:type_name -> changes.CalculatedLabelsTimelineEntry - 37, // 26: changes.ChangeTimelineEntryV2.formHypotheses:type_name -> changes.FormHypothesesTimelineEntry - 38, // 27: changes.ChangeTimelineEntryV2.investigateHypotheses:type_name -> changes.InvestigateHypothesesTimelineEntry - 36, // 28: changes.ChangeTimelineEntryV2.recordObservations:type_name -> changes.RecordObservationsTimelineEntry - 48, // 29: changes.MappedItemsTimelineEntry.mappedItems:type_name -> changes.MappedItemDiff - 39, // 30: changes.FormHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary - 39, // 31: changes.InvestigateHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary - 0, // 32: changes.HypothesisSummary.status:type_name -> changes.HypothesisStatus - 96, // 33: changes.CalculatedRisksTimelineEntry.risks:type_name -> changes.Risk - 62, // 34: changes.CalculatedLabelsTimelineEntry.labels:type_name -> changes.Label - 43, // 35: changes.ChangeValidationTimelineEntry.validationChecklist:type_name -> changes.ChangeValidationCategory - 57, // 36: changes.GetDiffResponse.expectedItems:type_name -> changes.ItemDiff - 57, // 37: changes.GetDiffResponse.unexpectedItems:type_name -> changes.ItemDiff - 105, // 38: changes.GetDiffResponse.edges:type_name -> Edge - 57, // 39: changes.GetDiffResponse.missingItems:type_name -> changes.ItemDiff - 56, // 40: changes.ListChangingItemsSummaryResponse.items:type_name -> changes.ItemDiffSummary - 57, // 41: changes.MappedItemDiff.item:type_name -> changes.ItemDiff - 106, // 42: changes.MappedItemDiff.mappingQuery:type_name -> Query - 107, // 43: changes.MappedItemDiff.mappingError:type_name -> QueryError - 48, // 44: changes.StartChangeAnalysisRequest.changingItems:type_name -> changes.MappedItemDiff - 108, // 45: changes.StartChangeAnalysisRequest.blastRadiusConfigOverride:type_name -> config.BlastRadiusConfig - 109, // 46: changes.StartChangeAnalysisRequest.routineChangesConfigOverride:type_name -> config.RoutineChangesConfig - 110, // 47: changes.StartChangeAnalysisRequest.githubOrganisationProfileOverride:type_name -> config.GithubOrganisationProfile - 111, // 48: changes.ListHomeChangesRequest.pagination:type_name -> PaginationRequest - 52, // 49: changes.ListHomeChangesRequest.filters:type_name -> changes.ChangeFiltersRequest - 8, // 50: changes.ChangeFiltersRequest.risks:type_name -> changes.Risk.Severity - 5, // 51: changes.ChangeFiltersRequest.statuses:type_name -> changes.ChangeStatus - 112, // 52: changes.ChangeFiltersRequest.sortOrder:type_name -> SortOrder - 63, // 53: changes.ListHomeChangesResponse.changes:type_name -> changes.ChangeSummary - 113, // 54: changes.ListHomeChangesResponse.pagination:type_name -> PaginationResponse - 114, // 55: changes.ItemDiffSummary.item:type_name -> Reference - 2, // 56: changes.ItemDiffSummary.status:type_name -> changes.ItemDiffStatus - 115, // 57: changes.ItemDiffSummary.healthAfter:type_name -> Health - 114, // 58: changes.ItemDiff.item:type_name -> Reference - 2, // 59: changes.ItemDiff.status:type_name -> changes.ItemDiffStatus - 116, // 60: changes.ItemDiff.before:type_name -> Item - 116, // 61: changes.ItemDiff.after:type_name -> Item - 100, // 62: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry - 60, // 63: changes.TagValue.userTagValue:type_name -> changes.UserTagValue - 61, // 64: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue - 4, // 65: changes.Label.type:type_name -> changes.LabelType - 5, // 66: changes.ChangeSummary.status:type_name -> changes.ChangeStatus - 104, // 67: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp - 101, // 68: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry - 58, // 69: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags - 62, // 70: changes.ChangeSummary.labels:type_name -> changes.Label - 67, // 71: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo - 65, // 72: changes.Change.metadata:type_name -> changes.ChangeMetadata - 66, // 73: changes.Change.properties:type_name -> changes.ChangeProperties - 104, // 74: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp - 104, // 75: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp - 5, // 76: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus - 102, // 77: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 102, // 78: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 102, // 79: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 102, // 80: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 102, // 81: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 67, // 82: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo - 57, // 83: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff - 103, // 84: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry - 58, // 85: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags - 62, // 86: changes.ChangeProperties.labels:type_name -> changes.Label - 64, // 87: changes.ListChangesResponse.changes:type_name -> changes.Change - 5, // 88: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus - 64, // 89: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change - 66, // 90: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties - 64, // 91: changes.CreateChangeResponse.change:type_name -> changes.Change - 3, // 92: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat - 8, // 93: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity - 3, // 94: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat - 64, // 95: changes.GetChangeResponse.change:type_name -> changes.Change - 97, // 96: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus - 96, // 97: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk - 82, // 98: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata - 66, // 99: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties - 64, // 100: changes.UpdateChangeResponse.change:type_name -> changes.Change - 64, // 101: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change - 6, // 102: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State - 7, // 103: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State - 8, // 104: changes.Risk.severity:type_name -> changes.Risk.Severity - 114, // 105: changes.Risk.relatedItems:type_name -> Reference - 9, // 106: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status - 59, // 107: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue - 68, // 108: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest - 70, // 109: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest - 72, // 110: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest - 74, // 111: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest - 75, // 112: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest - 76, // 113: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest - 30, // 114: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request - 81, // 115: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest - 84, // 116: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest - 86, // 117: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest - 87, // 118: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest - 90, // 119: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest - 92, // 120: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest - 94, // 121: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest - 51, // 122: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest - 49, // 123: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest - 46, // 124: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest - 44, // 125: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest - 54, // 126: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest - 98, // 127: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest - 27, // 128: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest - 78, // 129: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest - 13, // 130: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest - 15, // 131: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest - 17, // 132: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest - 19, // 133: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest - 21, // 134: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest - 23, // 135: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest - 25, // 136: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest - 69, // 137: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse - 71, // 138: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse - 73, // 139: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse - 80, // 140: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse - 80, // 141: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse - 77, // 142: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse - 31, // 143: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response - 83, // 144: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse - 85, // 145: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse - 89, // 146: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse - 88, // 147: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse - 91, // 148: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse - 93, // 149: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse - 95, // 150: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse - 53, // 151: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse - 50, // 152: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse - 47, // 153: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse - 45, // 154: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse - 55, // 155: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse - 99, // 156: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse - 28, // 157: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse - 79, // 158: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse - 14, // 159: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse - 16, // 160: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse - 18, // 161: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse - 20, // 162: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse - 22, // 163: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse - 24, // 164: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse - 26, // 165: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse - 137, // [137:166] is the sub-list for method output_type - 108, // [108:137] is the sub-list for method input_type - 108, // [108:108] is the sub-list for extension type_name - 108, // [108:108] is the sub-list for extension extendee - 0, // [0:108] is the sub-list for field type_name + 12, // 0: changes.LabelRule.metadata:type_name -> changes.LabelRuleMetadata + 13, // 1: changes.LabelRule.properties:type_name -> changes.LabelRuleProperties + 106, // 2: changes.LabelRuleMetadata.createdAt:type_name -> google.protobuf.Timestamp + 106, // 3: changes.LabelRuleMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 11, // 4: changes.ListLabelRulesResponse.rules:type_name -> changes.LabelRule + 13, // 5: changes.CreateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties + 11, // 6: changes.CreateLabelRuleResponse.rule:type_name -> changes.LabelRule + 11, // 7: changes.GetLabelRuleResponse.rule:type_name -> changes.LabelRule + 13, // 8: changes.UpdateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties + 11, // 9: changes.UpdateLabelRuleResponse.rule:type_name -> changes.LabelRule + 13, // 10: changes.TestLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties + 64, // 11: changes.TestLabelRuleResponse.label:type_name -> changes.Label + 106, // 12: changes.ReapplyLabelRuleInTimeRangeRequest.startAt:type_name -> google.protobuf.Timestamp + 106, // 13: changes.ReapplyLabelRuleInTimeRangeRequest.endAt:type_name -> google.protobuf.Timestamp + 30, // 14: changes.GetHypothesesDetailsResponse.hypotheses:type_name -> changes.HypothesesDetails + 1, // 15: changes.HypothesesDetails.status:type_name -> changes.HypothesisStatus + 33, // 16: changes.GetChangeTimelineV2Response.entries:type_name -> changes.ChangeTimelineEntryV2 + 2, // 17: changes.ChangeTimelineEntryV2.status:type_name -> changes.ChangeTimelineEntryStatus + 106, // 18: changes.ChangeTimelineEntryV2.startedAt:type_name -> google.protobuf.Timestamp + 106, // 19: changes.ChangeTimelineEntryV2.endedAt:type_name -> google.protobuf.Timestamp + 36, // 20: changes.ChangeTimelineEntryV2.mappedItems:type_name -> changes.MappedItemsTimelineEntry + 37, // 21: changes.ChangeTimelineEntryV2.calculatedBlastRadius:type_name -> changes.CalculatedBlastRadiusTimelineEntry + 42, // 22: changes.ChangeTimelineEntryV2.calculatedRisks:type_name -> changes.CalculatedRisksTimelineEntry + 34, // 23: changes.ChangeTimelineEntryV2.empty:type_name -> changes.EmptyContent + 44, // 24: changes.ChangeTimelineEntryV2.changeValidation:type_name -> changes.ChangeValidationTimelineEntry + 43, // 25: changes.ChangeTimelineEntryV2.calculatedLabels:type_name -> changes.CalculatedLabelsTimelineEntry + 39, // 26: changes.ChangeTimelineEntryV2.formHypotheses:type_name -> changes.FormHypothesesTimelineEntry + 40, // 27: changes.ChangeTimelineEntryV2.investigateHypotheses:type_name -> changes.InvestigateHypothesesTimelineEntry + 38, // 28: changes.ChangeTimelineEntryV2.recordObservations:type_name -> changes.RecordObservationsTimelineEntry + 0, // 29: changes.MappedItemTimelineSummary.status:type_name -> changes.MappedItemTimelineStatus + 50, // 30: changes.MappedItemsTimelineEntry.mappedItems:type_name -> changes.MappedItemDiff + 35, // 31: changes.MappedItemsTimelineEntry.items:type_name -> changes.MappedItemTimelineSummary + 41, // 32: changes.FormHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary + 41, // 33: changes.InvestigateHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary + 1, // 34: changes.HypothesisSummary.status:type_name -> changes.HypothesisStatus + 98, // 35: changes.CalculatedRisksTimelineEntry.risks:type_name -> changes.Risk + 64, // 36: changes.CalculatedLabelsTimelineEntry.labels:type_name -> changes.Label + 45, // 37: changes.ChangeValidationTimelineEntry.validationChecklist:type_name -> changes.ChangeValidationCategory + 59, // 38: changes.GetDiffResponse.expectedItems:type_name -> changes.ItemDiff + 59, // 39: changes.GetDiffResponse.unexpectedItems:type_name -> changes.ItemDiff + 107, // 40: changes.GetDiffResponse.edges:type_name -> Edge + 59, // 41: changes.GetDiffResponse.missingItems:type_name -> changes.ItemDiff + 58, // 42: changes.ListChangingItemsSummaryResponse.items:type_name -> changes.ItemDiffSummary + 59, // 43: changes.MappedItemDiff.item:type_name -> changes.ItemDiff + 108, // 44: changes.MappedItemDiff.mappingQuery:type_name -> Query + 109, // 45: changes.MappedItemDiff.mappingError:type_name -> QueryError + 50, // 46: changes.StartChangeAnalysisRequest.changingItems:type_name -> changes.MappedItemDiff + 110, // 47: changes.StartChangeAnalysisRequest.blastRadiusConfigOverride:type_name -> config.BlastRadiusConfig + 111, // 48: changes.StartChangeAnalysisRequest.routineChangesConfigOverride:type_name -> config.RoutineChangesConfig + 112, // 49: changes.StartChangeAnalysisRequest.githubOrganisationProfileOverride:type_name -> config.GithubOrganisationProfile + 113, // 50: changes.ListHomeChangesRequest.pagination:type_name -> PaginationRequest + 54, // 51: changes.ListHomeChangesRequest.filters:type_name -> changes.ChangeFiltersRequest + 9, // 52: changes.ChangeFiltersRequest.risks:type_name -> changes.Risk.Severity + 6, // 53: changes.ChangeFiltersRequest.statuses:type_name -> changes.ChangeStatus + 114, // 54: changes.ChangeFiltersRequest.sortOrder:type_name -> SortOrder + 65, // 55: changes.ListHomeChangesResponse.changes:type_name -> changes.ChangeSummary + 115, // 56: changes.ListHomeChangesResponse.pagination:type_name -> PaginationResponse + 116, // 57: changes.ItemDiffSummary.item:type_name -> Reference + 3, // 58: changes.ItemDiffSummary.status:type_name -> changes.ItemDiffStatus + 117, // 59: changes.ItemDiffSummary.healthAfter:type_name -> Health + 116, // 60: changes.ItemDiff.item:type_name -> Reference + 3, // 61: changes.ItemDiff.status:type_name -> changes.ItemDiffStatus + 118, // 62: changes.ItemDiff.before:type_name -> Item + 118, // 63: changes.ItemDiff.after:type_name -> Item + 102, // 64: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry + 62, // 65: changes.TagValue.userTagValue:type_name -> changes.UserTagValue + 63, // 66: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue + 5, // 67: changes.Label.type:type_name -> changes.LabelType + 6, // 68: changes.ChangeSummary.status:type_name -> changes.ChangeStatus + 106, // 69: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp + 103, // 70: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry + 60, // 71: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags + 64, // 72: changes.ChangeSummary.labels:type_name -> changes.Label + 69, // 73: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo + 67, // 74: changes.Change.metadata:type_name -> changes.ChangeMetadata + 68, // 75: changes.Change.properties:type_name -> changes.ChangeProperties + 106, // 76: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp + 106, // 77: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 6, // 78: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus + 104, // 79: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 104, // 80: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 104, // 81: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 104, // 82: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 104, // 83: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 69, // 84: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo + 59, // 85: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff + 105, // 86: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry + 60, // 87: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags + 64, // 88: changes.ChangeProperties.labels:type_name -> changes.Label + 66, // 89: changes.ListChangesResponse.changes:type_name -> changes.Change + 6, // 90: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus + 66, // 91: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change + 68, // 92: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties + 66, // 93: changes.CreateChangeResponse.change:type_name -> changes.Change + 4, // 94: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 9, // 95: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity + 4, // 96: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 66, // 97: changes.GetChangeResponse.change:type_name -> changes.Change + 99, // 98: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 98, // 99: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk + 84, // 100: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata + 68, // 101: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties + 66, // 102: changes.UpdateChangeResponse.change:type_name -> changes.Change + 66, // 103: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change + 7, // 104: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State + 8, // 105: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State + 9, // 106: changes.Risk.severity:type_name -> changes.Risk.Severity + 116, // 107: changes.Risk.relatedItems:type_name -> Reference + 10, // 108: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status + 61, // 109: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue + 70, // 110: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest + 72, // 111: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest + 74, // 112: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest + 76, // 113: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest + 77, // 114: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest + 78, // 115: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest + 31, // 116: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request + 83, // 117: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest + 86, // 118: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest + 88, // 119: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest + 89, // 120: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest + 92, // 121: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest + 94, // 122: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest + 96, // 123: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest + 53, // 124: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest + 51, // 125: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest + 48, // 126: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest + 46, // 127: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest + 56, // 128: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest + 100, // 129: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest + 28, // 130: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest + 80, // 131: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest + 14, // 132: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest + 16, // 133: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest + 18, // 134: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest + 20, // 135: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest + 22, // 136: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest + 24, // 137: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest + 26, // 138: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest + 71, // 139: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse + 73, // 140: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse + 75, // 141: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse + 82, // 142: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse + 82, // 143: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse + 79, // 144: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse + 32, // 145: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response + 85, // 146: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse + 87, // 147: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse + 91, // 148: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse + 90, // 149: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse + 93, // 150: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse + 95, // 151: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse + 97, // 152: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse + 55, // 153: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse + 52, // 154: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse + 49, // 155: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse + 47, // 156: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse + 57, // 157: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse + 101, // 158: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse + 29, // 159: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse + 81, // 160: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse + 15, // 161: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse + 17, // 162: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse + 19, // 163: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse + 21, // 164: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse + 23, // 165: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse + 25, // 166: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse + 27, // 167: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse + 139, // [139:168] is the sub-list for method output_type + 110, // [110:139] is the sub-list for method input_type + 110, // [110:110] is the sub-list for extension type_name + 110, // [110:110] is the sub-list for extension extendee + 0, // [0:110] is the sub-list for field type_name } func init() { file_changes_proto_init() } @@ -6899,23 +7037,24 @@ func file_changes_proto_init() { (*ChangeTimelineEntryV2_InvestigateHypotheses)(nil), (*ChangeTimelineEntryV2_RecordObservations)(nil), } - file_changes_proto_msgTypes[38].OneofWrappers = []any{} + file_changes_proto_msgTypes[24].OneofWrappers = []any{} file_changes_proto_msgTypes[39].OneofWrappers = []any{} - file_changes_proto_msgTypes[41].OneofWrappers = []any{} + file_changes_proto_msgTypes[40].OneofWrappers = []any{} file_changes_proto_msgTypes[42].OneofWrappers = []any{} - file_changes_proto_msgTypes[47].OneofWrappers = []any{} - file_changes_proto_msgTypes[49].OneofWrappers = []any{ + file_changes_proto_msgTypes[43].OneofWrappers = []any{} + file_changes_proto_msgTypes[48].OneofWrappers = []any{} + file_changes_proto_msgTypes[50].OneofWrappers = []any{ (*TagValue_UserTagValue)(nil), (*TagValue_AutoTagValue)(nil), } - file_changes_proto_msgTypes[55].OneofWrappers = []any{} + file_changes_proto_msgTypes[56].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: 10, - NumMessages: 94, + NumEnums: 11, + NumMessages: 95, NumExtensions: 0, NumServices: 2, }, diff --git a/sdp-go/changetimeline.go b/sdp-go/changetimeline.go index 2c1a1c19..d76c5b4f 100644 --- a/sdp-go/changetimeline.go +++ b/sdp-go/changetimeline.go @@ -2,8 +2,8 @@ package sdp // If you add/delete/move an entry here, make sure to update/check the following: // - the PopulateChangeTimelineV2 function -// - GetChangeTimelineV2 in api-server/server/changesservice.go -// - resetChangeAnalysisTables in api-server/server/changeanalysis/shared.go +// - GetChangeTimelineV2 in api-server/service/changesservice.go +// - resetChangeAnalysisTables in api-server/service/changeanalysis/shared.go // - the cli tool if we are waiting for a change analysis to finish // - frontend/src/features/changes-v2/change-timeline/ChangeTimeline.tsx - also update the entryNames object as this is used for comparing entry names // All timeline entries are now defined using ChangeTimelineEntryV2ID variables below. @@ -24,6 +24,10 @@ type ChangeTimelineEntryV2ID struct { Name string } +// if you add/delete/move an entry here, make sure to update/check the following: +// - changeTimelineEntryNameInProgress +// - changeTimelineEntryNameInProgressReverse +// - allChangeTimelineEntryV2IDs var ( // This is the entry that is created when we map the resources for a change, // this happens before we start blast radius simulation, it involves taking @@ -31,7 +35,7 @@ var ( // gateway to see whether any of them resolve into real items. ChangeTimelineEntryV2IDMapResources = ChangeTimelineEntryV2ID{ Label: "mapped_resources", - Name: "Mapping resources...", + Name: "Map resources", } // This is the entry that is created when we calculate the blast radius for a // change, this happens after we map the resources for a change, it involves @@ -39,14 +43,14 @@ var ( // simulation to see how many items are in the blast radius. ChangeTimelineEntryV2IDCalculatedBlastRadius = ChangeTimelineEntryV2ID{ Label: "calculated_blast_radius", - Name: "Simulating blast radius...", + Name: "Simulate blast radius", } // we do not show this entry in the timeline anymore // This is the entry tracks the calculation of routine signals for all of // the modifications within this change ChangeTimelineEntryV2IDAnalyzedSignals = ChangeTimelineEntryV2ID{ Label: "calculated_routineness", - Name: "Calculating routine signals...", + Name: "Analyze signals", } // This is the entry that tracks the calculation of risks and returns them // in the timeline. At the time of writing this has been replaced and we are @@ -61,7 +65,7 @@ var ( // Tracks the application of auto-label rules for a change ChangeTimelineEntryV2IDCalculatedLabels = ChangeTimelineEntryV2ID{ Label: "calculated_labels", - Name: "Applying auto labels...", + Name: "Apply auto labels", } // Tracks the application of auto tags for a change ChangeTimelineEntryV2IDAutoTagging = ChangeTimelineEntryV2ID{ @@ -77,16 +81,84 @@ var ( // This is the entry that tracks observations being recorded during blast radius simulation ChangeTimelineEntryV2IDRecordObservations = ChangeTimelineEntryV2ID{ Label: "record_observations", - Name: "Recording observations...", + Name: "Record observations", } // This is the entry that tracks hypotheses being formed from observations via batch processing ChangeTimelineEntryV2IDFormHypotheses = ChangeTimelineEntryV2ID{ Label: "form_hypotheses", - Name: "Forming hypotheses...", + Name: "Form hypotheses", } // This is the entry that tracks investigation of hypotheses via one-shot analysis ChangeTimelineEntryV2IDInvestigateHypotheses = ChangeTimelineEntryV2ID{ Label: "investigate_hypotheses", - Name: "Investigating hypotheses...", + Name: "Investigate hypotheses", } ) + +// changeTimelineEntryNameInProgress maps default/done names to their in-progress equivalents. +// This map is used to convert timeline entry names based on their status. +var changeTimelineEntryNameInProgress = map[string]string{ + "Map resources": "Mapping resources...", + "Simulate blast radius": "Simulating blast radius...", + "Record observations": "Recording observations...", + "Form hypotheses": "Forming hypotheses...", + "Investigate hypotheses": "Investigating hypotheses...", + "Analyze signals": "Analyzing signals...", + "Apply auto labels": "Applying auto labels...", +} + +// changeTimelineEntryNameInProgressReverse maps in-progress names back to their default/done equivalents. +// This is used for archive imports where we need to normalize names to look up labels. +var changeTimelineEntryNameInProgressReverse = func() map[string]string { + reverse := make(map[string]string, len(changeTimelineEntryNameInProgress)) + for defaultName, inProgressName := range changeTimelineEntryNameInProgress { + reverse[inProgressName] = defaultName + } + return reverse +}() + +// allChangeTimelineEntryV2IDs is a slice of all timeline entry ID constants for iteration. +var allChangeTimelineEntryV2IDs = []ChangeTimelineEntryV2ID{ + ChangeTimelineEntryV2IDMapResources, + ChangeTimelineEntryV2IDCalculatedBlastRadius, + ChangeTimelineEntryV2IDAnalyzedSignals, + ChangeTimelineEntryV2IDCalculatedRisks, + ChangeTimelineEntryV2IDCalculatedLabels, + ChangeTimelineEntryV2IDAutoTagging, + ChangeTimelineEntryV2IDChangeValidation, + ChangeTimelineEntryV2IDRecordObservations, + ChangeTimelineEntryV2IDFormHypotheses, + ChangeTimelineEntryV2IDInvestigateHypotheses, +} + +// GetChangeTimelineEntryNameForStatus returns the appropriate name for a timeline entry +// based on its status. If the status is IN_PROGRESS, it returns the in-progress name. +// Otherwise, it returns the name as-is (which is the default/done name). +func GetChangeTimelineEntryNameForStatus(name string, status ChangeTimelineEntryStatus) string { + if status == ChangeTimelineEntryStatus_IN_PROGRESS { + if inProgressName, ok := changeTimelineEntryNameInProgress[name]; ok { + return inProgressName + } + } + return name +} + +// GetChangeTimelineEntryLabelFromName converts a timeline entry name (either in-progress or default/done) +// to its corresponding label. This is used for archive imports where we need to match names to labels. +// Returns an empty string if the name doesn't match any known timeline entry. +func GetChangeTimelineEntryLabelFromName(name string) string { + // First, normalize the name: if it's an in-progress name, convert it to default/done name + normalizedName := name + if defaultName, ok := changeTimelineEntryNameInProgressReverse[name]; ok { + normalizedName = defaultName + } + + // Then look up the label from the constants + for _, entryID := range allChangeTimelineEntryV2IDs { + if entryID.Name == normalizedName { + return entryID.Label + } + } + + return "" +} diff --git a/sdp-go/changetimeline_test.go b/sdp-go/changetimeline_test.go new file mode 100644 index 00000000..391d4cb4 --- /dev/null +++ b/sdp-go/changetimeline_test.go @@ -0,0 +1,154 @@ +package sdp + +import "testing" + +// TestChangeTimelineEntryNameConversion tests both GetChangeTimelineEntryNameForStatus +// and GetChangeTimelineEntryLabelFromName together, including round-trip conversions. +func TestChangeTimelineEntryNameConversion(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + entryID ChangeTimelineEntryV2ID + hasInProgressVariant bool + }{ + { + name: "Map resources", + entryID: ChangeTimelineEntryV2IDMapResources, + hasInProgressVariant: true, + }, + { + name: "Simulate blast radius", + entryID: ChangeTimelineEntryV2IDCalculatedBlastRadius, + hasInProgressVariant: true, + }, + { + name: "Record observations", + entryID: ChangeTimelineEntryV2IDRecordObservations, + hasInProgressVariant: true, + }, + { + name: "Form hypotheses", + entryID: ChangeTimelineEntryV2IDFormHypotheses, + hasInProgressVariant: true, + }, + { + name: "Investigate hypotheses", + entryID: ChangeTimelineEntryV2IDInvestigateHypotheses, + hasInProgressVariant: true, + }, + { + name: "Analyze signals", + entryID: ChangeTimelineEntryV2IDAnalyzedSignals, + hasInProgressVariant: true, + }, + { + name: "Apply auto labels", + entryID: ChangeTimelineEntryV2IDCalculatedLabels, + hasInProgressVariant: true, + }, + { + name: "Calculated risks (no in-progress variant)", + entryID: ChangeTimelineEntryV2IDCalculatedRisks, + hasInProgressVariant: false, + }, + { + name: "Auto Tagging (no in-progress variant)", + entryID: ChangeTimelineEntryV2IDAutoTagging, + hasInProgressVariant: false, + }, + { + name: "Change Validation (no in-progress variant)", + entryID: ChangeTimelineEntryV2IDChangeValidation, + hasInProgressVariant: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defaultName := tt.entryID.Name + expectedLabel := tt.entryID.Label + + // Test 1: Default name -> IN_PROGRESS -> in-progress name + if tt.hasInProgressVariant { + gotInProgressName := GetChangeTimelineEntryNameForStatus(defaultName, ChangeTimelineEntryStatus_IN_PROGRESS) + // Verify that the in-progress name is different from the default name + if gotInProgressName == defaultName { + t.Errorf("GetChangeTimelineEntryNameForStatus(%q, IN_PROGRESS) should return in-progress name, got %q", defaultName, gotInProgressName) + } + // Verify it ends with "..." to indicate in-progress + if len(gotInProgressName) < 3 || gotInProgressName[len(gotInProgressName)-3:] != "..." { + t.Errorf("GetChangeTimelineEntryNameForStatus(%q, IN_PROGRESS) = %q, expected in-progress name ending with '...'", defaultName, gotInProgressName) + } + expectedInProgressName := gotInProgressName // Use the function result as the expected value + + // Test 2: In-progress name -> label (for archive imports) + gotLabelFromInProgress := GetChangeTimelineEntryLabelFromName(expectedInProgressName) + if gotLabelFromInProgress != expectedLabel { + t.Errorf("GetChangeTimelineEntryLabelFromName(%q) = %q, want %q", expectedInProgressName, gotLabelFromInProgress, expectedLabel) + } + + // Test 3: Round-trip: default -> in-progress -> label -> should match expected label + inProgressName := GetChangeTimelineEntryNameForStatus(defaultName, ChangeTimelineEntryStatus_IN_PROGRESS) + labelFromRoundTrip := GetChangeTimelineEntryLabelFromName(inProgressName) + if labelFromRoundTrip != expectedLabel { + t.Errorf("Round-trip: default(%q) -> in-progress(%q) -> label(%q), want label %q", defaultName, inProgressName, labelFromRoundTrip, expectedLabel) + } + } + + // Test 4: Default name -> DONE status -> should return default name + gotDoneName := GetChangeTimelineEntryNameForStatus(defaultName, ChangeTimelineEntryStatus_DONE) + if gotDoneName != defaultName { + t.Errorf("GetChangeTimelineEntryNameForStatus(%q, DONE) = %q, want %q", defaultName, gotDoneName, defaultName) + } + + // Test 5: Default name -> PENDING status -> should return default name + gotPendingName := GetChangeTimelineEntryNameForStatus(defaultName, ChangeTimelineEntryStatus_PENDING) + if gotPendingName != defaultName { + t.Errorf("GetChangeTimelineEntryNameForStatus(%q, PENDING) = %q, want %q", defaultName, gotPendingName, defaultName) + } + + // Test 6: Default name -> ERROR status -> should return default name + gotErrorName := GetChangeTimelineEntryNameForStatus(defaultName, ChangeTimelineEntryStatus_ERROR) + if gotErrorName != defaultName { + t.Errorf("GetChangeTimelineEntryNameForStatus(%q, ERROR) = %q, want %q", defaultName, gotErrorName, defaultName) + } + + // Test 7: Default name -> UNSPECIFIED status -> should return default name + gotUnspecifiedName := GetChangeTimelineEntryNameForStatus(defaultName, ChangeTimelineEntryStatus_UNSPECIFIED) + if gotUnspecifiedName != defaultName { + t.Errorf("GetChangeTimelineEntryNameForStatus(%q, UNSPECIFIED) = %q, want %q", defaultName, gotUnspecifiedName, defaultName) + } + + // Test 8: Default name -> label (for archive imports) + gotLabelFromDefault := GetChangeTimelineEntryLabelFromName(defaultName) + if gotLabelFromDefault != expectedLabel { + t.Errorf("GetChangeTimelineEntryLabelFromName(%q) = %q, want %q", defaultName, gotLabelFromDefault, expectedLabel) + } + }) + } + + // Test edge cases + t.Run("Unknown name with IN_PROGRESS returns name as-is", func(t *testing.T) { + unknownName := "Unknown Entry" + result := GetChangeTimelineEntryNameForStatus(unknownName, ChangeTimelineEntryStatus_IN_PROGRESS) + if result != unknownName { + t.Errorf("GetChangeTimelineEntryNameForStatus(%q, IN_PROGRESS) = %q, want %q", unknownName, result, unknownName) + } + }) + + t.Run("Unknown name returns empty label", func(t *testing.T) { + unknownName := "Unknown Entry" + result := GetChangeTimelineEntryLabelFromName(unknownName) + if result != "" { + t.Errorf("GetChangeTimelineEntryLabelFromName(%q) = %q, want empty string", unknownName, result) + } + }) + + t.Run("Empty string returns empty label", func(t *testing.T) { + result := GetChangeTimelineEntryLabelFromName("") + if result != "" { + t.Errorf("GetChangeTimelineEntryLabelFromName(\"\") = %q, want empty string", result) + } + }) +} diff --git a/sdp-go/config.pb.go b/sdp-go/config.pb.go index 0d4a6eb7..a3f022db 100644 --- a/sdp-go/config.pb.go +++ b/sdp-go/config.pb.go @@ -27,8 +27,8 @@ const ( type AccountConfig_BlastRadiusPreset int32 const ( - // Customise advanced limits. - AccountConfig_CUSTOM AccountConfig_BlastRadiusPreset = 0 + // Unspecified preset - will be treated as DETAILED + AccountConfig_UNSPECIFIED AccountConfig_BlastRadiusPreset = 0 // Runs a shallow scan for dependencies. Reduces time takes to calculate // blast radius, but might mean some dependencies are missed AccountConfig_QUICK AccountConfig_BlastRadiusPreset = 1 @@ -42,16 +42,16 @@ const ( // Enum value maps for AccountConfig_BlastRadiusPreset. var ( AccountConfig_BlastRadiusPreset_name = map[int32]string{ - 0: "CUSTOM", + 0: "UNSPECIFIED", 1: "QUICK", 2: "DETAILED", 3: "FULL", } AccountConfig_BlastRadiusPreset_value = map[string]int32{ - "CUSTOM": 0, - "QUICK": 1, - "DETAILED": 2, - "FULL": 3, + "UNSPECIFIED": 0, + "QUICK": 1, + "DETAILED": 2, + "FULL": 3, } ) @@ -197,13 +197,15 @@ type BlastRadiusConfig struct { // is the maximum number of levels of links to traverse from the root item. // Different implementations may differ in how they handle this. LinkDepth int32 `protobuf:"varint,2,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` - // Maximum time duration for change analysis. When this time limit is reached, the analysis will be cancelled. - // Blast radius calculation has a soft limit that is a percentage of the max timeout, this is currently set to 2/3 of the max timeout. - // It returns the results up to that point. - // Minimum recommended: 1 minute, Maximum recommended: 30 minutes. - ChangeAnalysisMaxTimeout *durationpb.Duration `protobuf:"bytes,4,opt,name=changeAnalysisMaxTimeout,proto3,oneof" json:"changeAnalysisMaxTimeout,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Target duration for change analysis planning and blast radius soft timeout calculation. + // This is NOT a hard deadline - it is used to compute when the blast radius phase should + // stop gracefully (at 67% of this target) so the remaining steps can complete around the target time. + // The actual job is only hard-limited by the service's maximum timeout (30 minutes). + // If the analysis runs slightly over this target, results are still returned. + // Minimum: 1 minute, Maximum: 30 minutes. + ChangeAnalysisTargetDuration *durationpb.Duration `protobuf:"bytes,4,opt,name=changeAnalysisTargetDuration,proto3,oneof" json:"changeAnalysisTargetDuration,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BlastRadiusConfig) Reset() { @@ -250,26 +252,24 @@ func (x *BlastRadiusConfig) GetLinkDepth() int32 { return 0 } -func (x *BlastRadiusConfig) GetChangeAnalysisMaxTimeout() *durationpb.Duration { +func (x *BlastRadiusConfig) GetChangeAnalysisTargetDuration() *durationpb.Duration { if x != nil { - return x.ChangeAnalysisMaxTimeout + return x.ChangeAnalysisTargetDuration } return nil } -// This account config is stored in the `kv.Store` protobuf key-value store in -// the api-server database. This means that as long as we don't have any -// *breaking* changes to the protobuf, we shouldn't need to do a migration. If -// however we do need to change this message in a breaking way, we will need to -// do some kind of a migration (depending on the change) +// Account configuration for blast radius settings. The blast radius preset +// is stored in the accounts table. Custom blast radius values are no longer +// supported - only preset values are used. type AccountConfig struct { state protoimpl.MessageState `protogen:"open.v1"` // The preset that we should use when calculating the blast radius for a - // change. If this is set to "CUSTOM" then the `blastRadius` config should be - // set + // change. UNSPECIFIED is treated as DETAILED. BlastRadiusPreset AccountConfig_BlastRadiusPreset `protobuf:"varint,2,opt,name=blastRadiusPreset,proto3,enum=config.AccountConfig_BlastRadiusPreset" json:"blastRadiusPreset,omitempty"` - // The blast radius config for this account, this is only required if the - // preset is "CUSTOM" + // The blast radius config for this account. This field is populated with + // hardcoded values based on the preset when reading. Custom values are + // ignored when writing - only the preset is stored. BlastRadius *BlastRadiusConfig `protobuf:"bytes,1,opt,name=blastRadius,proto3,oneof" json:"blastRadius,omitempty"` // If this is set to true, changes that weren't able to be mapped to real // infrastructure won't be considered for risk calculations. This usually @@ -315,7 +315,7 @@ func (x *AccountConfig) GetBlastRadiusPreset() AccountConfig_BlastRadiusPreset { if x != nil { return x.BlastRadiusPreset } - return AccountConfig_CUSTOM + return AccountConfig_UNSPECIFIED } func (x *AccountConfig) GetBlastRadius() *BlastRadiusConfig { @@ -1585,19 +1585,18 @@ var File_config_proto protoreflect.FileDescriptor const file_config_proto_rawDesc = "" + "\n" + - "\fconfig.proto\x12\x06config\x1a\rapikeys.proto\x1a\x1bbuf/validate/validate.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcc\x01\n" + + "\fconfig.proto\x12\x06config\x1a\rapikeys.proto\x1a\x1bbuf/validate/validate.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd8\x01\n" + "\x11BlastRadiusConfig\x12\x1a\n" + "\bmaxItems\x18\x01 \x01(\x05R\bmaxItems\x12\x1c\n" + - "\tlinkDepth\x18\x02 \x01(\x05R\tlinkDepth\x12Z\n" + - "\x18changeAnalysisMaxTimeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationH\x00R\x18changeAnalysisMaxTimeout\x88\x01\x01B\x1b\n" + - "\x19_changeAnalysisMaxTimeoutJ\x04\b\x03\x10\x04\"\xbe\x02\n" + + "\tlinkDepth\x18\x02 \x01(\x05R\tlinkDepth\x12b\n" + + "\x1cchangeAnalysisTargetDuration\x18\x04 \x01(\v2\x19.google.protobuf.DurationH\x00R\x1cchangeAnalysisTargetDuration\x88\x01\x01B\x1f\n" + + "\x1d_changeAnalysisTargetDurationJ\x04\b\x03\x10\x04\"\xc3\x02\n" + "\rAccountConfig\x12U\n" + "\x11blastRadiusPreset\x18\x02 \x01(\x0e2'.config.AccountConfig.BlastRadiusPresetR\x11blastRadiusPreset\x12@\n" + "\vblastRadius\x18\x01 \x01(\v2\x19.config.BlastRadiusConfigH\x00R\vblastRadius\x88\x01\x01\x12@\n" + - "\x1bskipUnmappedChangesForRisks\x18\x03 \x01(\bR\x1bskipUnmappedChangesForRisks\"B\n" + - "\x11BlastRadiusPreset\x12\n" + - "\n" + - "\x06CUSTOM\x10\x00\x12\t\n" + + "\x1bskipUnmappedChangesForRisks\x18\x03 \x01(\bR\x1bskipUnmappedChangesForRisks\"G\n" + + "\x11BlastRadiusPreset\x12\x0f\n" + + "\vUNSPECIFIED\x10\x00\x12\t\n" + "\x05QUICK\x10\x01\x12\f\n" + "\bDETAILED\x10\x02\x12\b\n" + "\x04FULL\x10\x03B\x0e\n" + @@ -1738,7 +1737,7 @@ var file_config_proto_goTypes = []any{ (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp } var file_config_proto_depIdxs = []int32{ - 31, // 0: config.BlastRadiusConfig.changeAnalysisMaxTimeout:type_name -> google.protobuf.Duration + 31, // 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 diff --git a/sdp-go/handler_cancelquery.go b/sdp-go/handler_cancelquery.go index ac3f9fd2..921a58dc 100644 --- a/sdp-go/handler_cancelquery.go +++ b/sdp-go/handler_cancelquery.go @@ -6,8 +6,8 @@ import ( "context" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/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/sdp-go/handler_gatewayresponse.go index d6016caf..776f22bd 100644 --- a/sdp-go/handler_gatewayresponse.go +++ b/sdp-go/handler_gatewayresponse.go @@ -6,8 +6,8 @@ import ( "context" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/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/sdp-go/handler_natsgetlogrecordsrequest.go index 405ea83b..64e08127 100644 --- a/sdp-go/handler_natsgetlogrecordsrequest.go +++ b/sdp-go/handler_natsgetlogrecordsrequest.go @@ -6,8 +6,8 @@ import ( "context" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/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/sdp-go/handler_natsgetlogrecordsresponse.go index dbcf524e..1af8bb5e 100644 --- a/sdp-go/handler_natsgetlogrecordsresponse.go +++ b/sdp-go/handler_natsgetlogrecordsresponse.go @@ -6,8 +6,8 @@ import ( "context" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/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/sdp-go/handler_query.go index a47cb45e..3bd8adca 100644 --- a/sdp-go/handler_query.go +++ b/sdp-go/handler_query.go @@ -6,8 +6,8 @@ import ( "context" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/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/sdp-go/handler_queryresponse.go index c17d2def..2f8c309c 100644 --- a/sdp-go/handler_queryresponse.go +++ b/sdp-go/handler_queryresponse.go @@ -6,8 +6,8 @@ import ( "context" "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/tracing" ) func NewQueryResponseHandler(spanName string, h func(ctx context.Context, i *QueryResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { diff --git a/sdp-go/items.go b/sdp-go/items.go index 8b0a6b5b..8f09d3c9 100644 --- a/sdp-go/items.go +++ b/sdp-go/items.go @@ -13,6 +13,8 @@ import ( "time" "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/types/known/structpb" ) @@ -273,6 +275,18 @@ func (r *Query) GetUUIDParsed() uuid.UUID { return reqUUID } +func (q *Query) SetSpanAttributes(span trace.Span) { + span.SetAttributes( + attribute.String("ovm.sdp.method", q.GetMethod().String()), + attribute.String("ovm.sdp.type", q.GetType()), + attribute.String("ovm.sdp.scope", q.GetScope()), + attribute.String("ovm.sdp.query", q.GetQuery()), + attribute.String("ovm.sdp.uuid", q.GetUUIDParsed().String()), + attribute.String("ovm.sdp.deadline", q.GetDeadline().AsTime().String()), + attribute.Bool("ovm.sdp.queryIgnoreCache", q.GetIgnoreCache()), + ) +} + func NewQueryResponseFromItem(item *Item) *QueryResponse { return &QueryResponse{ ResponseType: &QueryResponse_NewItem{ diff --git a/sdp-go/progress.go b/sdp-go/progress.go index 5516188d..e009fcd8 100644 --- a/sdp-go/progress.go +++ b/sdp-go/progress.go @@ -13,7 +13,6 @@ import ( "github.com/nats-io/nats.go" "github.com/overmindtech/cli/tracing" log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/types/known/durationpb" ) @@ -353,13 +352,7 @@ func RunSourceQuery(ctx context.Context, query *Query, startTimeout time.Duratio ctx, span := tracing.Tracer().Start(ctx, "QueryProgress") defer span.End() - // Attach query information to the span - span.SetAttributes( - attribute.String("ovm.sdp.type", query.GetType()), - attribute.String("ovm.sdp.scope", query.GetScope()), - attribute.String("ovm.sdp.uuid", uuid.UUID(query.GetUUID()).String()), - attribute.String("ovm.sdp.method", query.GetMethod().String()), - ) + query.SetSpanAttributes(span) for { select { diff --git a/sdp-go/signal.pb.go b/sdp-go/signal.pb.go index 71d91948..4866cabd 100644 --- a/sdp-go/signal.pb.go +++ b/sdp-go/signal.pb.go @@ -1005,6 +1005,75 @@ func (x *Signal) GetProperties() *SignalProperties { return nil } +// Structured output for GetChangeSummary JSON format +type ChangeSummaryJSONOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Change *Change `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` + Risks []*Risk `protobuf:"bytes,2,rep,name=risks,proto3" json:"risks,omitempty"` + Signals *GetChangeOverviewSignalsResponse `protobuf:"bytes,3,opt,name=signals,proto3" json:"signals,omitempty"` + Hypotheses []*HypothesesDetails `protobuf:"bytes,4,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeSummaryJSONOutput) Reset() { + *x = ChangeSummaryJSONOutput{} + mi := &file_signal_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeSummaryJSONOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeSummaryJSONOutput) ProtoMessage() {} + +func (x *ChangeSummaryJSONOutput) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[19] + 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 ChangeSummaryJSONOutput.ProtoReflect.Descriptor instead. +func (*ChangeSummaryJSONOutput) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{19} +} + +func (x *ChangeSummaryJSONOutput) GetChange() *Change { + if x != nil { + return x.Change + } + return nil +} + +func (x *ChangeSummaryJSONOutput) GetRisks() []*Risk { + if x != nil { + return x.Risks + } + return nil +} + +func (x *ChangeSummaryJSONOutput) GetSignals() *GetChangeOverviewSignalsResponse { + if x != nil { + return x.Signals + } + return nil +} + +func (x *ChangeSummaryJSONOutput) GetHypotheses() []*HypothesesDetails { + if x != nil { + return x.Hypotheses + } + return nil +} + var File_signal_proto protoreflect.FileDescriptor const file_signal_proto_rawDesc = "" + @@ -1087,7 +1156,14 @@ const file_signal_proto_rawDesc = "" + "\bmetadata\x18\x01 \x01(\v2\x16.signal.SignalMetadataR\bmetadata\x128\n" + "\n" + "properties\x18\x02 \x01(\v2\x18.signal.SignalPropertiesR\n" + - "properties2\xbb\x05\n" + + "properties\"\xe7\x01\n" + + "\x17ChangeSummaryJSONOutput\x12'\n" + + "\x06change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06change\x12#\n" + + "\x05risks\x18\x02 \x03(\v2\r.changes.RiskR\x05risks\x12B\n" + + "\asignals\x18\x03 \x01(\v2(.signal.GetChangeOverviewSignalsResponseR\asignals\x12:\n" + + "\n" + + "hypotheses\x18\x04 \x03(\v2\x1a.changes.HypothesesDetailsR\n" + + "hypotheses2\xbb\x05\n" + "\rSignalService\x12@\n" + "\tAddSignal\x12\x18.signal.AddSignalRequest\x1a\x19.signal.AddSignalResponse\x12y\n" + "\x1cGetSignalsByChangeExternalID\x12+.signal.GetSignalsByChangeExternalIDRequest\x1a,.signal.GetSignalsByChangeExternalIDResponse\x12m\n" + @@ -1109,7 +1185,7 @@ func file_signal_proto_rawDescGZIP() []byte { return file_signal_proto_rawDescData } -var file_signal_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_signal_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_signal_proto_goTypes = []any{ (*AddSignalRequest)(nil), // 0: signal.AddSignalRequest (*AddSignalResponse)(nil), // 1: signal.AddSignalResponse @@ -1130,9 +1206,13 @@ var file_signal_proto_goTypes = []any{ (*SignalMetadata)(nil), // 16: signal.SignalMetadata (*SignalProperties)(nil), // 17: signal.SignalProperties (*Signal)(nil), // 18: signal.Signal - nil, // 19: signal.GetItemSignalsResponse.ItemAggregationsEntry - (*Reference)(nil), // 20: Reference - (ItemDiffStatus)(0), // 21: changes.ItemDiffStatus + (*ChangeSummaryJSONOutput)(nil), // 19: signal.ChangeSummaryJSONOutput + nil, // 20: signal.GetItemSignalsResponse.ItemAggregationsEntry + (*Reference)(nil), // 21: Reference + (ItemDiffStatus)(0), // 22: changes.ItemDiffStatus + (*Change)(nil), // 23: changes.Change + (*Risk)(nil), // 24: changes.Risk + (*HypothesesDetails)(nil), // 25: changes.HypothesesDetails } var file_signal_proto_depIdxs = []int32{ 17, // 0: signal.AddSignalRequest.properties:type_name -> signal.SignalProperties @@ -1140,38 +1220,42 @@ var file_signal_proto_depIdxs = []int32{ 18, // 2: signal.GetSignalsByChangeExternalIDResponse.signals:type_name -> signal.Signal 18, // 3: signal.GetChangeOverviewSignalsResponse.signals:type_name -> signal.Signal 18, // 4: signal.ItemAggregation.signals:type_name -> signal.Signal - 19, // 5: signal.GetItemSignalsResponse.itemAggregations:type_name -> signal.GetItemSignalsResponse.ItemAggregationsEntry + 20, // 5: signal.GetItemSignalsResponse.itemAggregations:type_name -> signal.GetItemSignalsResponse.ItemAggregationsEntry 18, // 6: signal.ItemAggregationV2.signals:type_name -> signal.Signal - 20, // 7: signal.ItemAggregationV2.mappedRef:type_name -> Reference - 20, // 8: signal.ItemAggregationV2.afterRef:type_name -> Reference - 21, // 9: signal.ItemAggregationV2.status:type_name -> changes.ItemDiffStatus + 21, // 7: signal.ItemAggregationV2.mappedRef:type_name -> Reference + 21, // 8: signal.ItemAggregationV2.afterRef:type_name -> Reference + 22, // 9: signal.ItemAggregationV2.status:type_name -> changes.ItemDiffStatus 10, // 10: signal.GetItemSignalsResponseV2.itemAggregations:type_name -> signal.ItemAggregationV2 18, // 11: signal.GetCustomSignalsByCategoryResponse.signals:type_name -> signal.Signal - 20, // 12: signal.GetItemSignalDetailsRequest.item:type_name -> Reference + 21, // 12: signal.GetItemSignalDetailsRequest.item:type_name -> Reference 18, // 13: signal.GetItemSignalDetailsResponse.signals:type_name -> signal.Signal - 20, // 14: signal.SignalProperties.item:type_name -> Reference + 21, // 14: signal.SignalProperties.item:type_name -> Reference 16, // 15: signal.Signal.metadata:type_name -> signal.SignalMetadata 17, // 16: signal.Signal.properties:type_name -> signal.SignalProperties - 6, // 17: signal.GetItemSignalsResponse.ItemAggregationsEntry.value:type_name -> signal.ItemAggregation - 0, // 18: signal.SignalService.AddSignal:input_type -> signal.AddSignalRequest - 2, // 19: signal.SignalService.GetSignalsByChangeExternalID:input_type -> signal.GetSignalsByChangeExternalIDRequest - 4, // 20: signal.SignalService.GetChangeOverviewSignals:input_type -> signal.GetChangeOverviewSignalsRequest - 7, // 21: signal.SignalService.GetItemSignals:input_type -> signal.GetItemSignalsRequest - 9, // 22: signal.SignalService.GetItemSignalsV2:input_type -> signal.GetItemSignalsRequestV2 - 12, // 23: signal.SignalService.GetCustomSignalsByCategory:input_type -> signal.GetCustomSignalsByCategoryRequest - 14, // 24: signal.SignalService.GetItemSignalDetails:input_type -> signal.GetItemSignalDetailsRequest - 1, // 25: signal.SignalService.AddSignal:output_type -> signal.AddSignalResponse - 3, // 26: signal.SignalService.GetSignalsByChangeExternalID:output_type -> signal.GetSignalsByChangeExternalIDResponse - 5, // 27: signal.SignalService.GetChangeOverviewSignals:output_type -> signal.GetChangeOverviewSignalsResponse - 8, // 28: signal.SignalService.GetItemSignals:output_type -> signal.GetItemSignalsResponse - 11, // 29: signal.SignalService.GetItemSignalsV2:output_type -> signal.GetItemSignalsResponseV2 - 13, // 30: signal.SignalService.GetCustomSignalsByCategory:output_type -> signal.GetCustomSignalsByCategoryResponse - 15, // 31: signal.SignalService.GetItemSignalDetails:output_type -> signal.GetItemSignalDetailsResponse - 25, // [25:32] is the sub-list for method output_type - 18, // [18:25] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 23, // 17: signal.ChangeSummaryJSONOutput.change:type_name -> changes.Change + 24, // 18: signal.ChangeSummaryJSONOutput.risks:type_name -> changes.Risk + 5, // 19: signal.ChangeSummaryJSONOutput.signals:type_name -> signal.GetChangeOverviewSignalsResponse + 25, // 20: signal.ChangeSummaryJSONOutput.hypotheses:type_name -> changes.HypothesesDetails + 6, // 21: signal.GetItemSignalsResponse.ItemAggregationsEntry.value:type_name -> signal.ItemAggregation + 0, // 22: signal.SignalService.AddSignal:input_type -> signal.AddSignalRequest + 2, // 23: signal.SignalService.GetSignalsByChangeExternalID:input_type -> signal.GetSignalsByChangeExternalIDRequest + 4, // 24: signal.SignalService.GetChangeOverviewSignals:input_type -> signal.GetChangeOverviewSignalsRequest + 7, // 25: signal.SignalService.GetItemSignals:input_type -> signal.GetItemSignalsRequest + 9, // 26: signal.SignalService.GetItemSignalsV2:input_type -> signal.GetItemSignalsRequestV2 + 12, // 27: signal.SignalService.GetCustomSignalsByCategory:input_type -> signal.GetCustomSignalsByCategoryRequest + 14, // 28: signal.SignalService.GetItemSignalDetails:input_type -> signal.GetItemSignalDetailsRequest + 1, // 29: signal.SignalService.AddSignal:output_type -> signal.AddSignalResponse + 3, // 30: signal.SignalService.GetSignalsByChangeExternalID:output_type -> signal.GetSignalsByChangeExternalIDResponse + 5, // 31: signal.SignalService.GetChangeOverviewSignals:output_type -> signal.GetChangeOverviewSignalsResponse + 8, // 32: signal.SignalService.GetItemSignals:output_type -> signal.GetItemSignalsResponse + 11, // 33: signal.SignalService.GetItemSignalsV2:output_type -> signal.GetItemSignalsResponseV2 + 13, // 34: signal.SignalService.GetCustomSignalsByCategory:output_type -> signal.GetCustomSignalsByCategoryResponse + 15, // 35: signal.SignalService.GetItemSignalDetails:output_type -> signal.GetItemSignalDetailsResponse + 29, // [29:36] is the sub-list for method output_type + 22, // [22:29] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_signal_proto_init() } @@ -1188,7 +1272,7 @@ func file_signal_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_signal_proto_rawDesc), len(file_signal_proto_rawDesc)), NumEnums: 0, - NumMessages: 20, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/sources/azure/clients/application-gateways-client.go b/sources/azure/clients/application-gateways-client.go index fafbfe60..e755f3dd 100644 --- a/sources/azure/clients/application-gateways-client.go +++ b/sources/azure/clients/application-gateways-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" ) //go:generate mockgen -destination=../shared/mocks/mock_application_gateways_client.go -package=mocks -source=application-gateways-client.go diff --git a/sources/azure/clients/batch-accounts-client.go b/sources/azure/clients/batch-accounts-client.go index f9df0b50..3c41029f 100644 --- a/sources/azure/clients/batch-accounts-client.go +++ b/sources/azure/clients/batch-accounts-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" ) //go:generate mockgen -destination=../shared/mocks/mock_batch_accounts_client.go -package=mocks -source=batch-accounts-client.go diff --git a/sources/azure/clients/blob-containers-client.go b/sources/azure/clients/blob-containers-client.go index 0c62a2c2..b89708d8 100644 --- a/sources/azure/clients/blob-containers-client.go +++ b/sources/azure/clients/blob-containers-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" ) //go:generate mockgen -destination=../shared/mocks/mock_blob_containers_client.go -package=mocks -source=blob-containers-client.go diff --git a/sources/azure/clients/fileshares-client.go b/sources/azure/clients/fileshares-client.go index 8e686a95..e465a1a5 100644 --- a/sources/azure/clients/fileshares-client.go +++ b/sources/azure/clients/fileshares-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" ) //go:generate mockgen -destination=../shared/mocks/mock_file_shares_client.go -package=mocks -source=fileshares-client.go diff --git a/sources/azure/clients/load-balancers-client.go b/sources/azure/clients/load-balancers-client.go index 0bc6f874..2ba33b1e 100644 --- a/sources/azure/clients/load-balancers-client.go +++ b/sources/azure/clients/load-balancers-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" ) //go:generate mockgen -destination=../shared/mocks/mock_load_balancers_client.go -package=mocks -source=load-balancers-client.go diff --git a/sources/azure/clients/managed-hsms-client.go b/sources/azure/clients/managed-hsms-client.go index 3e6a446f..18f1a30d 100644 --- a/sources/azure/clients/managed-hsms-client.go +++ b/sources/azure/clients/managed-hsms-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" ) //go:generate mockgen -destination=../shared/mocks/mock_managed_hsms_client.go -package=mocks -source=managed-hsms-client.go diff --git a/sources/azure/clients/network-interfaces-client.go b/sources/azure/clients/network-interfaces-client.go index e2f94d5b..cbc8963c 100644 --- a/sources/azure/clients/network-interfaces-client.go +++ b/sources/azure/clients/network-interfaces-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" ) //go:generate mockgen -destination=../shared/mocks/mock_network_interfaces_client.go -package=mocks -source=network-interfaces-client.go diff --git a/sources/azure/clients/network-security-groups-client.go b/sources/azure/clients/network-security-groups-client.go index eb609996..7dc9df0e 100644 --- a/sources/azure/clients/network-security-groups-client.go +++ b/sources/azure/clients/network-security-groups-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" ) //go:generate mockgen -destination=../shared/mocks/mock_network_security_groups_client.go -package=mocks -source=network-security-groups-client.go diff --git a/sources/azure/clients/pager_mocks.go b/sources/azure/clients/pager_mocks.go index fb2384f8..d0199367 100644 --- a/sources/azure/clients/pager_mocks.go +++ b/sources/azure/clients/pager_mocks.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" ) // These interfaces are defined specifically for mock generation. @@ -13,6 +13,7 @@ import ( // that match the Pager[T] interface for specific types. // VirtualMachinesPagerInterface is a concrete interface for VirtualMachinesPager to enable mock generation +// //go:generate mockgen -destination=../shared/mocks/mock_virtual_machines_pager.go -package=mocks github.com/overmindtech/cli/sources/azure/clients VirtualMachinesPagerInterface type VirtualMachinesPagerInterface interface { More() bool @@ -20,6 +21,7 @@ type VirtualMachinesPagerInterface interface { } // StorageAccountsPagerInterface is a concrete interface for StorageAccountsPager to enable mock generation +// //go:generate mockgen -destination=../shared/mocks/mock_storage_accounts_pager.go -package=mocks github.com/overmindtech/cli/sources/azure/clients StorageAccountsPagerInterface type StorageAccountsPagerInterface interface { More() bool diff --git a/sources/azure/clients/proximity-placement-groups-client.go b/sources/azure/clients/proximity-placement-groups-client.go new file mode 100644 index 00000000..a478f4ab --- /dev/null +++ b/sources/azure/clients/proximity-placement-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_proximity_placement_groups_client.go -package=mocks -source=proximity-placement-groups-client.go + +// ProximityPlacementGroupsPager is a type alias for the generic Pager interface with proximity placement group response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type ProximityPlacementGroupsPager = Pager[armcompute.ProximityPlacementGroupsClientListByResourceGroupResponse] + +// ProximityPlacementGroupsClient is an interface for interacting with Azure proximity placement groups +type ProximityPlacementGroupsClient interface { + ListByResourceGroup(ctx context.Context, resourceGroupName string, options *armcompute.ProximityPlacementGroupsClientListByResourceGroupOptions) ProximityPlacementGroupsPager + Get(ctx context.Context, resourceGroupName string, proximityPlacementGroupName string, options *armcompute.ProximityPlacementGroupsClientGetOptions) (armcompute.ProximityPlacementGroupsClientGetResponse, error) +} + +type proximityPlacementGroupsClient struct { + client *armcompute.ProximityPlacementGroupsClient +} + +func (a *proximityPlacementGroupsClient) ListByResourceGroup(ctx context.Context, resourceGroupName string, options *armcompute.ProximityPlacementGroupsClientListByResourceGroupOptions) ProximityPlacementGroupsPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *proximityPlacementGroupsClient) Get(ctx context.Context, resourceGroupName string, proximityPlacementGroupName string, options *armcompute.ProximityPlacementGroupsClientGetOptions) (armcompute.ProximityPlacementGroupsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, proximityPlacementGroupName, options) +} + +// NewProximityPlacementGroupsClient creates a new ProximityPlacementGroupsClient from the Azure SDK client +func NewProximityPlacementGroupsClient(client *armcompute.ProximityPlacementGroupsClient) ProximityPlacementGroupsClient { + return &proximityPlacementGroupsClient{client: client} +} diff --git a/sources/azure/clients/public-ip-addresses.go b/sources/azure/clients/public-ip-addresses.go index 62cd53e4..c5b29958 100644 --- a/sources/azure/clients/public-ip-addresses.go +++ b/sources/azure/clients/public-ip-addresses.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" ) //go:generate mockgen -destination=../shared/mocks/mock_public_ip_addresses_client.go -package=mocks -source=public-ip-addresses.go diff --git a/sources/azure/clients/queues-client.go b/sources/azure/clients/queues-client.go index de3f6864..d0ea8c54 100644 --- a/sources/azure/clients/queues-client.go +++ b/sources/azure/clients/queues-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" ) //go:generate mockgen -destination=../shared/mocks/mock_queues_client.go -package=mocks -source=queues-client.go diff --git a/sources/azure/clients/route-tables-client.go b/sources/azure/clients/route-tables-client.go index cb08f9eb..2708b3c3 100644 --- a/sources/azure/clients/route-tables-client.go +++ b/sources/azure/clients/route-tables-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" ) //go:generate mockgen -destination=../shared/mocks/mock_route_tables_client.go -package=mocks -source=route-tables-client.go diff --git a/sources/azure/clients/secrets-client.go b/sources/azure/clients/secrets-client.go index f2b7da8c..448f63dc 100644 --- a/sources/azure/clients/secrets-client.go +++ b/sources/azure/clients/secrets-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" ) //go:generate mockgen -destination=../shared/mocks/mock_secrets_client.go -package=mocks -source=secrets-client.go diff --git a/sources/azure/clients/sql-databases-client.go b/sources/azure/clients/sql-databases-client.go index a107893d..1999057c 100644 --- a/sources/azure/clients/sql-databases-client.go +++ b/sources/azure/clients/sql-databases-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" ) //go:generate mockgen -destination=../shared/mocks/mock_sql_databases_client.go -package=mocks -source=sql-databases-client.go diff --git a/sources/azure/clients/sql-servers-client.go b/sources/azure/clients/sql-servers-client.go index ea3e3846..a4a470f9 100644 --- a/sources/azure/clients/sql-servers-client.go +++ b/sources/azure/clients/sql-servers-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" ) //go:generate mockgen -destination=../shared/mocks/mock_sql_servers_client.go -package=mocks -source=sql-servers-client.go diff --git a/sources/azure/clients/storage-accounts-client.go b/sources/azure/clients/storage-accounts-client.go index 0a9c8f02..075bd1b0 100644 --- a/sources/azure/clients/storage-accounts-client.go +++ b/sources/azure/clients/storage-accounts-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" ) //go:generate mockgen -destination=../shared/mocks/mock_storage_accounts_client.go -package=mocks -source=storage-accounts-client.go diff --git a/sources/azure/clients/tables-client.go b/sources/azure/clients/tables-client.go index 8270891e..b68d4117 100644 --- a/sources/azure/clients/tables-client.go +++ b/sources/azure/clients/tables-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" ) //go:generate mockgen -destination=../shared/mocks/mock_tables_client.go -package=mocks -source=tables-client.go diff --git a/sources/azure/clients/vaults-client.go b/sources/azure/clients/vaults-client.go index e4be16d9..7009f0f3 100644 --- a/sources/azure/clients/vaults-client.go +++ b/sources/azure/clients/vaults-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" ) //go:generate mockgen -destination=../shared/mocks/mock_vaults_client.go -package=mocks -source=vaults-client.go diff --git a/sources/azure/clients/virtual-networks-client.go b/sources/azure/clients/virtual-networks-client.go index f790cc92..07e99710 100644 --- a/sources/azure/clients/virtual-networks-client.go +++ b/sources/azure/clients/virtual-networks-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" ) //go:generate mockgen -destination=../shared/mocks/mock_virtual_networks_client.go -package=mocks -source=virtual-networks-client.go diff --git a/sources/azure/cmd/root.go b/sources/azure/cmd/root.go index be33eb60..78cde3ec 100644 --- a/sources/azure/cmd/root.go +++ b/sources/azure/cmd/root.go @@ -3,12 +3,10 @@ package cmd import ( "context" "fmt" - "net/http" "os" "os/signal" "strings" "syscall" - "time" "github.com/getsentry/sentry-go" "github.com/overmindtech/cli/logging" @@ -53,35 +51,7 @@ var rootCmd = &cobra.Command{ e.StartSendingHeartbeats(ctx) - // Start HTTP server for health checks - // Liveness: Check only engine initialization (NATS, heartbeats) - http.HandleFunc("/healthz/alive", e.LivenessProbeHandlerFunc()) - // Readiness: Check if adapters are healthy and ready to handle requests - http.HandleFunc("/healthz/ready", e.ReadinessProbeHandlerFunc()) - // Backward compatibility - maps to liveness check (matches old behavior) - http.HandleFunc("/healthz", e.LivenessProbeHandlerFunc()) - - log.WithFields(log.Fields{ - "ovm.source.type": "azure", - "ovm.source.port": healthCheckPort, - }).Debug("Starting healthcheck server with endpoints: /healthz/alive, /healthz/ready, /healthz") - - go func() { - defer sentry.Recover() - - server := &http.Server{ - Addr: fmt.Sprintf(":%v", healthCheckPort), - Handler: nil, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - err := server.ListenAndServe() - - log.WithError(err).WithFields(log.Fields{ - "ovm.source.type": "azure", - "ovm.source.port": healthCheckPort, - }).Error("Could not start HTTP server for health checks") - }() + e.ServeHealthProbes(healthCheckPort) err = e.Start(ctx) if err != nil { diff --git a/sources/azure/integration-tests/batch-batch-accounts_test.go b/sources/azure/integration-tests/batch-batch-accounts_test.go index 3cc9a9f8..0610403f 100644 --- a/sources/azure/integration-tests/batch-batch-accounts_test.go +++ b/sources/azure/integration-tests/batch-batch-accounts_test.go @@ -12,9 +12,9 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/compute-availability-set_test.go b/sources/azure/integration-tests/compute-availability-set_test.go index 638f4466..e3054038 100644 --- a/sources/azure/integration-tests/compute-availability-set_test.go +++ b/sources/azure/integration-tests/compute-availability-set_test.go @@ -11,7 +11,7 @@ import ( "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/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" 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 f62c75c2..5e41b600 100644 --- a/sources/azure/integration-tests/compute-disk-encryption-set_test.go +++ b/sources/azure/integration-tests/compute-disk-encryption-set_test.go @@ -11,7 +11,7 @@ import ( "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/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" diff --git a/sources/azure/integration-tests/compute-proximity-placement-group_test.go b/sources/azure/integration-tests/compute-proximity-placement-group_test.go new file mode 100644 index 00000000..66ef4913 --- /dev/null +++ b/sources/azure/integration-tests/compute-proximity-placement-group_test.go @@ -0,0 +1,349 @@ +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/discovery" + "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/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 ( + integrationTestProximityPlacementGroupName = "ovm-integ-test-ppg" +) + +func TestComputeProximityPlacementGroupIntegration(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) + } + + ppgClient, err := armcompute.NewProximityPlacementGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Proximity Placement 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 = createProximityPlacementGroup(ctx, ppgClient, integrationTestResourceGroup, integrationTestProximityPlacementGroupName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create proximity placement group: %v", err) + } + + err = waitForProximityPlacementGroupAvailable(ctx, ppgClient, integrationTestResourceGroup, integrationTestProximityPlacementGroupName) + if err != nil { + t.Fatalf("Failed waiting for proximity placement group to be available: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + ctx := t.Context() + _, err := ppgClient.Get(ctx, integrationTestResourceGroup, integrationTestProximityPlacementGroupName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + t.Skipf("Proximity placement group %s does not exist - Setup may have failed. Skipping Run tests.", integrationTestProximityPlacementGroupName) + } + } + + t.Run("GetProximityPlacementGroup", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving proximity placement group %s in subscription %s, resource group %s", + integrationTestProximityPlacementGroupName, subscriptionID, integrationTestResourceGroup) + + ppgWrapper := manual.NewComputeProximityPlacementGroup( + clients.NewProximityPlacementGroupsClient(ppgClient), + subscriptionID, + integrationTestResourceGroup, + ) + scope := ppgWrapper.Scopes()[0] + + ppgAdapter := sources.WrapperToAdapter(ppgWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := ppgAdapter.Get(ctx, scope, integrationTestProximityPlacementGroupName, 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 != integrationTestProximityPlacementGroupName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestProximityPlacementGroupName, uniqueAttrValue) + } + + if sdpItem.GetType() != azureshared.ComputeProximityPlacementGroup.String() { + t.Fatalf("Expected type %s, got %s", azureshared.ComputeProximityPlacementGroup.String(), sdpItem.GetType()) + } + + log.Printf("Successfully retrieved proximity placement group %s", integrationTestProximityPlacementGroupName) + }) + + t.Run("ListProximityPlacementGroups", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing proximity placement groups in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + ppgWrapper := manual.NewComputeProximityPlacementGroup( + clients.NewProximityPlacementGroupsClient(ppgClient), + subscriptionID, + integrationTestResourceGroup, + ) + scope := ppgWrapper.Scopes()[0] + + ppgAdapter := sources.WrapperToAdapter(ppgWrapper, sdpcache.NewNoOpCache()) + + listable, ok := ppgAdapter.(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 proximity placement groups: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one proximity placement 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 == integrationTestProximityPlacementGroupName { + found = true + if item.GetType() != azureshared.ComputeProximityPlacementGroup.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeProximityPlacementGroup.String(), item.GetType()) + } + break + } + } + + if !found { + t.Fatalf("Expected to find proximity placement group %s in the list", integrationTestProximityPlacementGroupName) + } + + log.Printf("Found %d proximity placement groups in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for proximity placement group %s", integrationTestProximityPlacementGroupName) + + ppgWrapper := manual.NewComputeProximityPlacementGroup( + clients.NewProximityPlacementGroupsClient(ppgClient), + subscriptionID, + integrationTestResourceGroup, + ) + scope := ppgWrapper.Scopes()[0] + + ppgAdapter := sources.WrapperToAdapter(ppgWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := ppgAdapter.Get(ctx, scope, integrationTestProximityPlacementGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for proximity placement group %s", len(linkedQueries), integrationTestProximityPlacementGroupName) + + 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 { + t.Errorf("Expected link method to be GET, got %s", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + 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()) + } + } + } + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for proximity placement group %s", integrationTestProximityPlacementGroupName) + + ppgWrapper := manual.NewComputeProximityPlacementGroup( + clients.NewProximityPlacementGroupsClient(ppgClient), + subscriptionID, + integrationTestResourceGroup, + ) + scope := ppgWrapper.Scopes()[0] + + ppgAdapter := sources.WrapperToAdapter(ppgWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := ppgAdapter.Get(ctx, scope, integrationTestProximityPlacementGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeProximityPlacementGroup.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeProximityPlacementGroup.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 proximity placement group %s", integrationTestProximityPlacementGroupName) + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + err := deleteProximityPlacementGroup(ctx, ppgClient, integrationTestResourceGroup, integrationTestProximityPlacementGroupName) + if err != nil { + t.Fatalf("Failed to delete proximity placement group: %v", err) + } + }) +} + +func createProximityPlacementGroup(ctx context.Context, client *armcompute.ProximityPlacementGroupsClient, resourceGroupName, ppgName, location string) error { + _, err := client.Get(ctx, resourceGroupName, ppgName, nil) + if err == nil { + log.Printf("Proximity placement group %s already exists, skipping creation", ppgName) + return nil + } + + resp, err := client.CreateOrUpdate(ctx, resourceGroupName, ppgName, armcompute.ProximityPlacementGroup{ + Location: ptr.To(location), + Properties: &armcompute.ProximityPlacementGroupProperties{ + ProximityPlacementGroupType: ptr.To(armcompute.ProximityPlacementGroupTypeStandard), + }, + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-proximity-placement-group"), + }, + }, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Proximity placement group %s already exists (conflict), skipping creation", ppgName) + return nil + } + return fmt.Errorf("failed to create proximity placement group: %w", err) + } + + if resp.Name == nil { + return fmt.Errorf("proximity placement group created but name is nil") + } + + log.Printf("Proximity placement group %s created successfully", ppgName) + return nil +} + +func waitForProximityPlacementGroupAvailable(ctx context.Context, client *armcompute.ProximityPlacementGroupsClient, resourceGroupName, ppgName string) error { + const maxAttempts = 10 + pollInterval := 2 * time.Second + + log.Printf("Waiting for proximity placement group %s to be available via API...", ppgName) + + for attempt := 1; attempt <= maxAttempts; attempt++ { + resp, err := client.Get(ctx, resourceGroupName, ppgName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Proximity placement group %s not yet available (attempt %d/%d), waiting %v...", ppgName, attempt, maxAttempts, pollInterval) + time.Sleep(pollInterval) + continue + } + return fmt.Errorf("error checking proximity placement group availability: %w", err) + } + + if resp.Name != nil { + log.Printf("Proximity placement group %s is available", ppgName) + return nil + } + + if attempt < maxAttempts { + log.Printf("Proximity placement group %s not yet ready (attempt %d/%d), waiting %v...", ppgName, attempt, maxAttempts, pollInterval) + time.Sleep(pollInterval) + continue + } + } + + return fmt.Errorf("timeout waiting for proximity placement group %s to be available after %d attempts", ppgName, maxAttempts) +} + +func deleteProximityPlacementGroup(ctx context.Context, client *armcompute.ProximityPlacementGroupsClient, resourceGroupName, ppgName string) error { + _, err := client.Delete(ctx, resourceGroupName, ppgName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Proximity placement group %s not found, skipping deletion", ppgName) + return nil + } + return fmt.Errorf("failed to delete proximity placement group: %w", err) + } + + log.Printf("Proximity placement group %s deleted successfully", ppgName) + 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 3fe65a7b..731ce0f5 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go @@ -11,7 +11,7 @@ import ( "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/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" 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 ee01d18e..942e0a88 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 @@ -11,7 +11,7 @@ import ( "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/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" 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 f8d55d5f..2f2d5f4e 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 @@ -12,7 +12,7 @@ import ( "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/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/compute-virtual-machine_test.go b/sources/azure/integration-tests/compute-virtual-machine_test.go index 6453f450..1c695570 100644 --- a/sources/azure/integration-tests/compute-virtual-machine_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine_test.go @@ -11,7 +11,7 @@ import ( "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/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/keyvault-managed-hsm_test.go b/sources/azure/integration-tests/keyvault-managed-hsm_test.go index d7a8e817..3a3d8281 100644 --- a/sources/azure/integration-tests/keyvault-managed-hsm_test.go +++ b/sources/azure/integration-tests/keyvault-managed-hsm_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" diff --git a/sources/azure/integration-tests/keyvault-secret_test.go b/sources/azure/integration-tests/keyvault-secret_test.go index 85cbd9b0..ef29775b 100644 --- a/sources/azure/integration-tests/keyvault-secret_test.go +++ b/sources/azure/integration-tests/keyvault-secret_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" diff --git a/sources/azure/integration-tests/keyvault-vault_test.go b/sources/azure/integration-tests/keyvault-vault_test.go index 6cde4f89..529e700b 100644 --- a/sources/azure/integration-tests/keyvault-vault_test.go +++ b/sources/azure/integration-tests/keyvault-vault_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-application-gateway_test.go b/sources/azure/integration-tests/network-application-gateway_test.go index 9037e41f..f9ea1571 100644 --- a/sources/azure/integration-tests/network-application-gateway_test.go +++ b/sources/azure/integration-tests/network-application-gateway_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-load-balancer_test.go b/sources/azure/integration-tests/network-load-balancer_test.go index cf27bddb..683ef648 100644 --- a/sources/azure/integration-tests/network-load-balancer_test.go +++ b/sources/azure/integration-tests/network-load-balancer_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-network-interface_test.go b/sources/azure/integration-tests/network-network-interface_test.go index 9ded5985..e9ec529a 100644 --- a/sources/azure/integration-tests/network-network-interface_test.go +++ b/sources/azure/integration-tests/network-network-interface_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" 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 432aab0f..a35ed229 100644 --- a/sources/azure/integration-tests/network-network-security-group_test.go +++ b/sources/azure/integration-tests/network-network-security-group_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" 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 638d97a7..e62e9503 100644 --- a/sources/azure/integration-tests/network-public-ip-address_test.go +++ b/sources/azure/integration-tests/network-public-ip-address_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-route-table_test.go b/sources/azure/integration-tests/network-route-table_test.go index 5e186986..da109c92 100644 --- a/sources/azure/integration-tests/network-route-table_test.go +++ b/sources/azure/integration-tests/network-route-table_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-virtual-network_test.go b/sources/azure/integration-tests/network-virtual-network_test.go index f809b405..6e823e67 100644 --- a/sources/azure/integration-tests/network-virtual-network_test.go +++ b/sources/azure/integration-tests/network-virtual-network_test.go @@ -5,7 +5,7 @@ import ( "os" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" diff --git a/sources/azure/integration-tests/sql-database_test.go b/sources/azure/integration-tests/sql-database_test.go index a4913e61..ae959a0a 100644 --- a/sources/azure/integration-tests/sql-database_test.go +++ b/sources/azure/integration-tests/sql-database_test.go @@ -13,7 +13,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/sql-server_test.go b/sources/azure/integration-tests/sql-server_test.go index 7b03226b..d7004a37 100644 --- a/sources/azure/integration-tests/sql-server_test.go +++ b/sources/azure/integration-tests/sql-server_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" log "github.com/sirupsen/logrus" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/integration-tests/storage-account_test.go b/sources/azure/integration-tests/storage-account_test.go index 8d34aabd..17872422 100644 --- a/sources/azure/integration-tests/storage-account_test.go +++ b/sources/azure/integration-tests/storage-account_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/integration-tests/storage-blob-container_test.go b/sources/azure/integration-tests/storage-blob-container_test.go index c776424f..5846d95c 100644 --- a/sources/azure/integration-tests/storage-blob-container_test.go +++ b/sources/azure/integration-tests/storage-blob-container_test.go @@ -13,7 +13,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/storage-fileshare_test.go b/sources/azure/integration-tests/storage-fileshare_test.go index ae814465..cf1c5453 100644 --- a/sources/azure/integration-tests/storage-fileshare_test.go +++ b/sources/azure/integration-tests/storage-fileshare_test.go @@ -10,7 +10,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/storage-queues_test.go b/sources/azure/integration-tests/storage-queues_test.go index ae429962..bcc75c8b 100644 --- a/sources/azure/integration-tests/storage-queues_test.go +++ b/sources/azure/integration-tests/storage-queues_test.go @@ -10,7 +10,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/integration-tests/storage-table_test.go b/sources/azure/integration-tests/storage-table_test.go index d4bfb6c2..c5f551ec 100644 --- a/sources/azure/integration-tests/storage-table_test.go +++ b/sources/azure/integration-tests/storage-table_test.go @@ -10,7 +10,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 31bdc8d6..2734a4ca 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -6,16 +6,16 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" "github.com/overmindtech/cli/discovery" @@ -221,6 +221,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create virtual machine extensions client: %w", err) } + proximityPlacementGroupsClient, err := armcompute.NewProximityPlacementGroupsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create proximity placement groups client: %w", err) + } + // Create adapters for each resource group for _, resourceGroup := range resourceGroups { // Add Compute Virtual Machine adapter for this resource group @@ -487,6 +492,14 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred resourceGroup, ), cache), ) + // Add Proximity Placement Group adapter for this resource group + adapters = append(adapters, + sources.WrapperToAdapter(NewComputeProximityPlacementGroup( + clients.NewProximityPlacementGroupsClient(proximityPlacementGroupsClient), + subscriptionID, + resourceGroup, + ), cache), + ) } log.WithFields(log.Fields{ @@ -654,6 +667,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred subscriptionID, "placeholder-resource-group", ), sdpcache.NewNoOpCache()), // no-op cache for metadata registration + sources.WrapperToAdapter(NewComputeProximityPlacementGroup( + nil, // nil client is okay for metadata registration + subscriptionID, + "placeholder-resource-group", + ), sdpcache.NewNoOpCache()), // no-op cache for metadata registration ) _ = regions diff --git a/sources/azure/manual/batch-batch-accounts.go b/sources/azure/manual/batch-batch-accounts.go index 48a20b8d..22461fe0 100644 --- a/sources/azure/manual/batch-batch-accounts.go +++ b/sources/azure/manual/batch-batch-accounts.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -372,13 +372,13 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun if account.Properties != nil && account.Properties.AccountEndpoint != nil && *account.Properties.AccountEndpoint != "" { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: "dns", + Type: stdlib.NetworkDNS.String(), Method: sdp.QueryMethod_SEARCH, Query: *account.Properties.AccountEndpoint, Scope: "global", }, BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked + // DNS names are shared resources; changes can affect connectivity both ways In: true, Out: true, }, @@ -392,16 +392,15 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun if ipRule != nil && ipRule.Value != nil && *ipRule.Value != "" { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: "ip", + Type: stdlib.NetworkIP.String(), Method: sdp.QueryMethod_GET, Query: *ipRule.Value, Scope: "global", }, BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on IP rules for network access control - // If IP rules change, batch account access may be affected + // IPs are shared resources; changes can affect connectivity both ways In: true, - Out: false, + Out: true, }, }) } @@ -416,16 +415,15 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun if ipRule != nil && ipRule.Value != nil && *ipRule.Value != "" { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: "ip", + Type: stdlib.NetworkIP.String(), Method: sdp.QueryMethod_GET, Query: *ipRule.Value, Scope: "global", }, BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on IP rules for node management access control - // If IP rules change, batch account node management access may be affected + // IPs are shared resources; changes can affect connectivity both ways In: true, - Out: false, + Out: true, }, }) } diff --git a/sources/azure/manual/batch-batch-accounts_test.go b/sources/azure/manual/batch-batch-accounts_test.go index 3d136507..ba7c06dd 100644 --- a/sources/azure/manual/batch-batch-accounts_test.go +++ b/sources/azure/manual/batch-batch-accounts_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/manual/compute-proximity-placement-group.go b/sources/azure/manual/compute-proximity-placement-group.go new file mode 100644 index 00000000..8ab9d101 --- /dev/null +++ b/sources/azure/manual/compute-proximity-placement-group.go @@ -0,0 +1,203 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/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" +) + +var ComputeProximityPlacementGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeProximityPlacementGroup) + +type computeProximityPlacementGroupWrapper struct { + client clients.ProximityPlacementGroupsClient + *azureshared.ResourceGroupBase +} + +func NewComputeProximityPlacementGroup(client clients.ProximityPlacementGroupsClient, subscriptionID, resourceGroup string) sources.ListableWrapper { + return &computeProximityPlacementGroupWrapper{ + client: client, + ResourceGroupBase: azureshared.NewResourceGroupBase( + subscriptionID, + resourceGroup, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeProximityPlacementGroup, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/proximity-placement-groups/list-by-resource-group?view=rest-compute-2025-04-01&tabs=HTTP +func (c computeProximityPlacementGroupWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + resourceGroup := azureshared.ResourceGroupFromScope(scope) + if resourceGroup == "" { + resourceGroup = c.ResourceGroup() + } + pager := c.client.ListByResourceGroup(ctx, 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 _, proximityPlacementGroup := range page.Value { + if proximityPlacementGroup.Name == nil { + continue + } + item, sdpErr := c.azureProximityPlacementGroupToSDPItem(proximityPlacementGroup, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/proximity-placement-groups/get?view=rest-compute-2025-04-01&tabs=HTTP +func (c computeProximityPlacementGroupWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + resourceGroup := azureshared.ResourceGroupFromScope(scope) + if resourceGroup == "" { + resourceGroup = c.ResourceGroup() + } + if len(queryParts) < 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be at least 1 and be the proximity placement group name"), scope, c.Type()) + } + proximityPlacementGroupName := queryParts[0] + resp, err := c.client.Get(ctx, resourceGroup, proximityPlacementGroupName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureProximityPlacementGroupToSDPItem(&resp.ProximityPlacementGroup, scope) +} + +func (c computeProximityPlacementGroupWrapper) azureProximityPlacementGroupToSDPItem(proximityPlacementGroup *armcompute.ProximityPlacementGroup, scope string) (*sdp.Item, *sdp.QueryError) { + if proximityPlacementGroup.Name == nil { + return nil, azureshared.QueryError(errors.New("proximityPlacementGroupName is nil"), scope, c.Type()) + } + attributes, err := shared.ToAttributesWithExclude(proximityPlacementGroup, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + sdpItem := &sdp.Item{ + Type: azureshared.ComputeProximityPlacementGroup.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(proximityPlacementGroup.Tags), + } + + // Link to Virtual Machines in the proximity placement group + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/virtual-machines/get + if proximityPlacementGroup.Properties != nil && proximityPlacementGroup.Properties.VirtualMachines != nil { + for _, ref := range proximityPlacementGroup.Properties.VirtualMachines { + if ref != nil && ref.ID != nil { + vmName := azureshared.ExtractResourceName(*ref.ID) + if vmName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(*ref.ID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeVirtualMachine.String(), + Method: sdp.QueryMethod_GET, + Query: vmName, + Scope: linkedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // PPG change affects VM placement + Out: true, // VM add/remove changes PPG membership + }, + }) + } + } + } + } + + // Link to Availability Sets in the proximity placement group + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/availability-sets/get + if proximityPlacementGroup.Properties != nil && proximityPlacementGroup.Properties.AvailabilitySets != nil { + for _, ref := range proximityPlacementGroup.Properties.AvailabilitySets { + if ref != nil && ref.ID != nil { + avSetName := azureshared.ExtractResourceName(*ref.ID) + if avSetName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(*ref.ID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeAvailabilitySet.String(), + Method: sdp.QueryMethod_GET, + Query: avSetName, + Scope: linkedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // PPG change affects Availability Set placement + Out: true, // Availability Set add/remove changes PPG membership + }, + }) + } + } + } + } + + // Link to Virtual Machine Scale Sets in the proximity placement group + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/virtual-machine-scale-sets/get + if proximityPlacementGroup.Properties != nil && proximityPlacementGroup.Properties.VirtualMachineScaleSets != nil { + for _, ref := range proximityPlacementGroup.Properties.VirtualMachineScaleSets { + if ref != nil && ref.ID != nil { + vmssName := azureshared.ExtractResourceName(*ref.ID) + if vmssName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(*ref.ID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeVirtualMachineScaleSet.String(), + Method: sdp.QueryMethod_GET, + Query: vmssName, + Scope: linkedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // PPG change affects VMSS placement + Out: true, // VMSS add/remove changes PPG membership + }, + }) + } + } + } + } + + return sdpItem, nil +} + +func (c computeProximityPlacementGroupWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeVirtualMachine: true, + azureshared.ComputeAvailabilitySet: true, + azureshared.ComputeVirtualMachineScaleSet: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/proximity_placement_group +func (c computeProximityPlacementGroupWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_proximity_placement_group.name", + }, + } +} + +func (c computeProximityPlacementGroupWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeProximityPlacementGroupLookupByName, + } +} diff --git a/sources/azure/manual/compute-proximity-placement-group_test.go b/sources/azure/manual/compute-proximity-placement-group_test.go new file mode 100644 index 00000000..3aef3639 --- /dev/null +++ b/sources/azure/manual/compute-proximity-placement-group_test.go @@ -0,0 +1,372 @@ +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/discovery" + "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/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 TestComputeProximityPlacementGroup(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) { + ppgName := "test-ppg" + ppg := createAzureProximityPlacementGroup(ppgName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockProximityPlacementGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, ppgName, nil).Return( + armcompute.ProximityPlacementGroupsClientGetResponse{ + ProximityPlacementGroup: *ppg, + }, nil) + + wrapper := manual.NewComputeProximityPlacementGroup(mockClient, subscriptionID, resourceGroup) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, ppgName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeProximityPlacementGroup.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeProximityPlacementGroup.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != ppgName { + t.Errorf("Expected unique attribute value %s, got %s", ppgName, 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.ComputeVirtualMachine.String(), + 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) + }) + }) + + t.Run("GetWithCrossResourceGroupLinks", func(t *testing.T) { + ppgName := "test-ppg-cross-rg" + ppg := createAzureProximityPlacementGroupWithCrossResourceGroupLinks(ppgName, subscriptionID) + + mockClient := mocks.NewMockProximityPlacementGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, ppgName, nil).Return( + armcompute.ProximityPlacementGroupsClientGetResponse{ + ProximityPlacementGroup: *ppg, + }, nil) + + wrapper := manual.NewComputeProximityPlacementGroup(mockClient, subscriptionID, resourceGroup) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, ppgName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + expectedVMScope := subscriptionID + ".vm-rg" + expectedAVSetScope := subscriptionID + ".avset-rg" + expectedVMSSScope := subscriptionID + ".vmss-rg" + + for _, link := range sdpItem.GetLinkedItemQueries() { + q := link.GetQuery() + switch q.GetType() { + case azureshared.ComputeVirtualMachine.String(): + if q.GetScope() != expectedVMScope { + t.Errorf("Expected VM scope %s, got %s", expectedVMScope, q.GetScope()) + } + case azureshared.ComputeAvailabilitySet.String(): + if q.GetScope() != expectedAVSetScope { + t.Errorf("Expected Availability Set scope %s, got %s", expectedAVSetScope, q.GetScope()) + } + case azureshared.ComputeVirtualMachineScaleSet.String(): + if q.GetScope() != expectedVMSSScope { + t.Errorf("Expected VMSS scope %s, got %s", expectedVMSSScope, q.GetScope()) + } + } + } + }) + + t.Run("GetWithoutLinks", func(t *testing.T) { + ppgName := "test-ppg-no-links" + ppg := createAzureProximityPlacementGroupWithoutLinks(ppgName) + + mockClient := mocks.NewMockProximityPlacementGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, ppgName, nil).Return( + armcompute.ProximityPlacementGroupsClientGetResponse{ + ProximityPlacementGroup: *ppg, + }, nil) + + wrapper := manual.NewComputeProximityPlacementGroup(mockClient, subscriptionID, resourceGroup) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, ppgName, 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) { + ppg1 := createAzureProximityPlacementGroup("test-ppg-1", subscriptionID, resourceGroup) + ppg2 := createAzureProximityPlacementGroup("test-ppg-2", subscriptionID, resourceGroup) + + mockClient := mocks.NewMockProximityPlacementGroupsClient(ctrl) + mockPager := newMockProximityPlacementGroupsPager(ctrl, []*armcompute.ProximityPlacementGroup{ppg1, ppg2}) + + mockClient.EXPECT().ListByResourceGroup(ctx, resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeProximityPlacementGroup(mockClient, 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"]) + } + } + }) + + // ListStream is not implemented for the proximity placement group adapter + // (wrapper does not implement ListStreamableWrapper), so no ListStream test. + + t.Run("ListWithNilName", func(t *testing.T) { + ppg1 := createAzureProximityPlacementGroup("test-ppg-1", subscriptionID, resourceGroup) + ppgNilName := &armcompute.ProximityPlacementGroup{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + } + + mockClient := mocks.NewMockProximityPlacementGroupsClient(ctrl) + mockPager := newMockProximityPlacementGroupsPager(ctrl, []*armcompute.ProximityPlacementGroup{ppg1, ppgNilName}) + + mockClient.EXPECT().ListByResourceGroup(ctx, resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeProximityPlacementGroup(mockClient, 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("GetError", func(t *testing.T) { + expectedErr := errors.New("proximity placement group not found") + + mockClient := mocks.NewMockProximityPlacementGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-ppg", nil).Return( + armcompute.ProximityPlacementGroupsClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeProximityPlacementGroup(mockClient, subscriptionID, resourceGroup) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent-ppg", true) + if qErr == nil { + t.Error("Expected error when getting non-existent proximity placement group, but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockProximityPlacementGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "", nil).Return( + armcompute.ProximityPlacementGroupsClientGetResponse{}, errors.New("proximity placement group name is required")) + + wrapper := manual.NewComputeProximityPlacementGroup(mockClient, subscriptionID, resourceGroup) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting proximity placement group with empty name, but got nil") + } + }) + +} + +func createAzureProximityPlacementGroup(ppgName, subscriptionID, resourceGroup string) *armcompute.ProximityPlacementGroup { + baseID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute" + return &armcompute.ProximityPlacementGroup{ + Name: to.Ptr(ppgName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.ProximityPlacementGroupProperties{ + ProximityPlacementGroupType: to.Ptr(armcompute.ProximityPlacementGroupTypeStandard), + VirtualMachines: []*armcompute.SubResourceWithColocationStatus{ + {ID: to.Ptr(baseID + "/virtualMachines/test-vm")}, + }, + AvailabilitySets: []*armcompute.SubResourceWithColocationStatus{ + {ID: to.Ptr(baseID + "/availabilitySets/test-avset")}, + }, + VirtualMachineScaleSets: []*armcompute.SubResourceWithColocationStatus{ + {ID: to.Ptr(baseID + "/virtualMachineScaleSets/test-vmss")}, + }, + }, + Zones: []*string{to.Ptr("1")}, + } +} + +func createAzureProximityPlacementGroupWithCrossResourceGroupLinks(ppgName, subscriptionID string) *armcompute.ProximityPlacementGroup { + return &armcompute.ProximityPlacementGroup{ + Name: to.Ptr(ppgName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.ProximityPlacementGroupProperties{ + ProximityPlacementGroupType: to.Ptr(armcompute.ProximityPlacementGroupTypeStandard), + VirtualMachines: []*armcompute.SubResourceWithColocationStatus{ + {ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/vm-rg/providers/Microsoft.Compute/virtualMachines/test-vm")}, + }, + AvailabilitySets: []*armcompute.SubResourceWithColocationStatus{ + {ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/avset-rg/providers/Microsoft.Compute/availabilitySets/test-avset")}, + }, + VirtualMachineScaleSets: []*armcompute.SubResourceWithColocationStatus{ + {ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/vmss-rg/providers/Microsoft.Compute/virtualMachineScaleSets/test-vmss")}, + }, + }, + } +} + +func createAzureProximityPlacementGroupWithoutLinks(ppgName string) *armcompute.ProximityPlacementGroup { + return &armcompute.ProximityPlacementGroup{ + Name: to.Ptr(ppgName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.ProximityPlacementGroupProperties{ + ProximityPlacementGroupType: to.Ptr(armcompute.ProximityPlacementGroupTypeStandard), + }, + } +} + +type mockProximityPlacementGroupsPager struct { + ctrl *gomock.Controller + items []*armcompute.ProximityPlacementGroup + index int + more bool +} + +func newMockProximityPlacementGroupsPager(ctrl *gomock.Controller, items []*armcompute.ProximityPlacementGroup) clients.ProximityPlacementGroupsPager { + return &mockProximityPlacementGroupsPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockProximityPlacementGroupsPager) More() bool { + return m.more +} + +func (m *mockProximityPlacementGroupsPager) NextPage(ctx context.Context) (armcompute.ProximityPlacementGroupsClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.ProximityPlacementGroupsClientListByResourceGroupResponse{ + ProximityPlacementGroupListResult: armcompute.ProximityPlacementGroupListResult{ + Value: []*armcompute.ProximityPlacementGroup{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.ProximityPlacementGroupsClientListByResourceGroupResponse{ + ProximityPlacementGroupListResult: armcompute.ProximityPlacementGroupListResult{ + Value: []*armcompute.ProximityPlacementGroup{item}, + }, + }, nil +} diff --git a/sources/azure/manual/dns_links.go b/sources/azure/manual/dns_links.go new file mode 100644 index 00000000..8f9f96da --- /dev/null +++ b/sources/azure/manual/dns_links.go @@ -0,0 +1,28 @@ +package manual + +import ( + "net" + + "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/sources/stdlib" +) + +// appendDNSServerLinkIfValid appends a linked item query for a DNS server string: +// stdlib.NetworkIP for IP addresses, stdlib.NetworkDNS for hostnames. +// Skips empty strings and any value in skipValues (e.g. "AzureProvidedDNS" for Azure managed DNS). +func appendDNSServerLinkIfValid(queries *[]*sdp.LinkedItemQuery, server string, skipValues ...string) { + appendLinkIfValid(queries, server, skipValues, func(s string) *sdp.LinkedItemQuery { + if net.ParseIP(s) != nil { + return networkIPQuery(s) + } + return &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: s, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + } + }) +} diff --git a/sources/azure/manual/keyvault-managed-hsm.go b/sources/azure/manual/keyvault-managed-hsm.go index 1349ece3..6b0ffaaf 100644 --- a/sources/azure/manual/keyvault-managed-hsm.go +++ b/sources/azure/manual/keyvault-managed-hsm.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "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" @@ -114,6 +114,31 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma LinkedItemQueries: []*sdp.LinkedItemQuery{}, } + // Link to MHSM Private Endpoint Connections (child resources with their own GET endpoint) + // Reference: https://learn.microsoft.com/en-us/rest/api/keyvault/managedhsm/mhsm-private-endpoint-connections/get + // GET .../managedHSMs/{name}/privateEndpointConnections/{privateEndpointConnectionName} + if hsm.Properties != nil && hsm.Properties.PrivateEndpointConnections != nil && hsm.Name != nil { + for _, conn := range hsm.Properties.PrivateEndpointConnections { + if conn != nil && conn.ID != nil && *conn.ID != "" { + connectionName := azureshared.ExtractResourceName(*conn.ID) + if connectionName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultManagedHSMPrivateEndpointConnection.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + } + } + // 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} @@ -134,18 +159,18 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma if privateEndpointName != "" { // Construct scope in format: {subscriptionID}.{resourceGroupName} // This ensures we query the correct resource group where the private endpoint actually exists - scope := fmt.Sprintf("%s.%s", subscriptionID, resourceGroupName) + peScope := fmt.Sprintf("%s.%s", subscriptionID, resourceGroupName) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ Type: azureshared.NetworkPrivateEndpoint.String(), Method: sdp.QueryMethod_GET, Query: privateEndpointName, - Scope: scope, // Use the private endpoint's scope, not the Managed HSM's scope + 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 - }, // Private endpoints are tightly coupled to the Managed HSM - changes affect connectivity + }, }) } } @@ -254,12 +279,13 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma // Link to DNS name (standard library) from HsmURI // The HsmURI contains the Managed HSM endpoint URL (e.g., https://myhsm.managedhsm.azure.net) if hsm.Properties != nil && hsm.Properties.HsmURI != nil && *hsm.Properties.HsmURI != "" { + hsmURI := *hsm.Properties.HsmURI // Extract DNS name from URL (e.g., https://myhsm.managedhsm.azure.net -> myhsm.managedhsm.azure.net) - dnsName := azureshared.ExtractDNSFromURL(*hsm.Properties.HsmURI) + dnsName := azureshared.ExtractDNSFromURL(hsmURI) if dnsName != "" { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: "dns", + Type: stdlib.NetworkDNS.String(), Method: sdp.QueryMethod_SEARCH, Query: dnsName, Scope: "global", @@ -271,6 +297,20 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma }, }) } + // Link to HTTP/HTTPS endpoint (standard library) from HsmURI + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: hsmURI, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + // Endpoint connectivity affects HSM access and vice versa + In: true, + Out: true, + }, + }) } return sdpItem, nil @@ -317,11 +357,13 @@ func (k keyvaultManagedHSMsWrapper) TerraformMappings() []*sdp.TerraformMapping func (k keyvaultManagedHSMsWrapper) PotentialLinks() map[shared.ItemType]bool { return map[shared.ItemType]bool{ - azureshared.NetworkPrivateEndpoint: true, - azureshared.NetworkSubnet: true, - azureshared.ManagedIdentityUserAssignedIdentity: true, - stdlib.NetworkDNS: true, - stdlib.NetworkIP: true, + azureshared.KeyVaultManagedHSMPrivateEndpointConnection: true, + azureshared.NetworkPrivateEndpoint: true, + azureshared.NetworkSubnet: true, + azureshared.ManagedIdentityUserAssignedIdentity: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + stdlib.NetworkIP: true, } } diff --git a/sources/azure/manual/keyvault-managed-hsm_test.go b/sources/azure/manual/keyvault-managed-hsm_test.go index a7bb4d4f..bb3d81ea 100644 --- a/sources/azure/manual/keyvault-managed-hsm_test.go +++ b/sources/azure/manual/keyvault-managed-hsm_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -108,6 +108,28 @@ func TestKeyVaultManagedHSM(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ + { + // MHSM Private Endpoint Connection (GET) - child resource + ExpectedType: azureshared.KeyVaultManagedHSMPrivateEndpointConnection.String(), + 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(), @@ -176,7 +198,7 @@ func TestKeyVaultManagedHSM(t *testing.T) { }, { // DNS (SEARCH) - from HsmURI - ExpectedType: "dns", + ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: hsmName + ".managedhsm.azure.net", ExpectedScope: "global", @@ -185,6 +207,17 @@ func TestKeyVaultManagedHSM(t *testing.T) { 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(), @@ -640,9 +673,10 @@ func createAzureManagedHSM(hsmName, subscriptionID, resourceGroup string) *armke Properties: &armkeyvault.ManagedHsmProperties{ TenantID: to.Ptr("test-tenant-id"), HsmURI: to.Ptr("https://" + hsmName + ".managedhsm.azure.net"), - // Private Endpoint Connections + // Private Endpoint Connections (ID is the connection resource ID for child resource linking) PrivateEndpointConnections: []*armkeyvault.MHSMPrivateEndpointConnectionItem{ { + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/managedHSMs/" + hsmName + "/privateEndpointConnections/test-pec-1"), Properties: &armkeyvault.MHSMPrivateEndpointConnectionProperties{ PrivateEndpoint: &armkeyvault.MHSMPrivateEndpoint{ ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), @@ -650,6 +684,7 @@ func createAzureManagedHSM(hsmName, subscriptionID, resourceGroup string) *armke }, }, { + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/managedHSMs/" + hsmName + "/privateEndpointConnections/test-pec-2"), Properties: &armkeyvault.MHSMPrivateEndpointConnectionProperties{ PrivateEndpoint: &armkeyvault.MHSMPrivateEndpoint{ ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-diff-rg"), diff --git a/sources/azure/manual/keyvault-secret.go b/sources/azure/manual/keyvault-secret.go index 6479be5f..27004cd9 100644 --- a/sources/azure/manual/keyvault-secret.go +++ b/sources/azure/manual/keyvault-secret.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -171,12 +171,14 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, } } - // Link to DNS name (standard library) from SecretURI - // The SecretURI contains the Key Vault secret endpoint URL (e.g., https://myvault.vault.azure.net/secrets/mysecret) + // Link to DNS name and HTTP endpoints (standard library) from SecretURI and SecretURIWithVersion. + // Both URIs share the same Key Vault hostname (e.g., myvault.vault.azure.net), so we add the DNS link only once. + var linkedDNSName string if secret.Properties != nil && secret.Properties.SecretURI != nil && *secret.Properties.SecretURI != "" { - // Extract DNS name from URL (e.g., https://myvault.vault.azure.net/secrets/mysecret -> myvault.vault.azure.net) - dnsName := azureshared.ExtractDNSFromURL(*secret.Properties.SecretURI) + secretURI := *secret.Properties.SecretURI + dnsName := azureshared.ExtractDNSFromURL(secretURI) if dnsName != "" { + linkedDNSName = dnsName sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ Type: stdlib.NetworkDNS.String(), @@ -185,12 +187,55 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Scope: "global", }, BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked bidirectionally In: true, Out: true, }, }) } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: secretURI, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + + // SecretURIWithVersion is the versioned URL; add HTTP link. Skip DNS link if same hostname already linked. + if secret.Properties != nil && secret.Properties.SecretURIWithVersion != nil && *secret.Properties.SecretURIWithVersion != "" { + secretURIWithVersion := *secret.Properties.SecretURIWithVersion + dnsName := azureshared.ExtractDNSFromURL(secretURIWithVersion) + if dnsName != "" && dnsName != linkedDNSName { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: dnsName, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: secretURIWithVersion, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) } return sdpItem, nil @@ -225,6 +270,7 @@ func (k keyvaultSecretWrapper) PotentialLinks() map[shared.ItemType]bool { return shared.NewItemTypesSet( azureshared.KeyVaultVault, stdlib.NetworkDNS, + stdlib.NetworkHTTP, ) } diff --git a/sources/azure/manual/keyvault-secret_test.go b/sources/azure/manual/keyvault-secret_test.go index db6b64f2..c89720c0 100644 --- a/sources/azure/manual/keyvault-secret_test.go +++ b/sources/azure/manual/keyvault-secret_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -128,6 +128,28 @@ func TestKeyVaultSecret(t *testing.T) { 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) @@ -447,6 +469,10 @@ func TestKeyVaultSecret(t *testing.T) { if !links[stdlib.NetworkDNS] { t.Error("Expected stdlib.NetworkDNS to be in potential links") } + + if !links[stdlib.NetworkHTTP] { + t.Error("Expected stdlib.NetworkHTTP to be in potential links") + } }) t.Run("TerraformMappings", func(t *testing.T) { @@ -575,7 +601,8 @@ func createAzureSecret(secretName, subscriptionID, resourceGroup, vaultName stri "project": to.Ptr("testing"), }, Properties: &armkeyvault.SecretProperties{ - Value: to.Ptr("secret-value"), + Value: to.Ptr("secret-value"), + SecretURI: to.Ptr(fmt.Sprintf("https://%s.vault.azure.net/secrets/%s", vaultName, secretName)), }, } } diff --git a/sources/azure/manual/keyvault-vault.go b/sources/azure/manual/keyvault-vault.go index 79bf4dae..3e1764d7 100644 --- a/sources/azure/manual/keyvault-vault.go +++ b/sources/azure/manual/keyvault-vault.go @@ -5,12 +5,13 @@ import ( "errors" "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/overmindtech/cli/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 KeyVaultVaultLookupByName = shared.NewItemTypeLookup("name", azureshared.KeyVaultVault) @@ -185,6 +186,46 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s } } + // Link to IP addresses (standard library) from NetworkACLs IPRules + // Reference: https://learn.microsoft.com/en-us/rest/api/keyvault/vaults/get + if vault.Properties != nil && vault.Properties.NetworkACLs != nil && vault.Properties.NetworkACLs.IPRules != nil { + for _, ipRule := range vault.Properties.NetworkACLs.IPRules { + if ipRule != nil && ipRule.Value != nil && *ipRule.Value != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *ipRule.Value, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + // IPs are always linked - IP rule changes affect Key Vault network accessibility + In: true, + Out: true, + }, + }) + } + } + } + + // Link to stdlib.NetworkHTTP for the vault URI (HTTPS endpoint for keys and secrets operations) + if vault.Properties != nil && vault.Properties.VaultURI != nil && *vault.Properties.VaultURI != "" { + vaultURI := *vault.Properties.VaultURI + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: vaultURI, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + // Vault endpoint connectivity affects Key Vault operations; Key Vault changes may affect endpoint + In: true, + Out: true, + }, + }) + } + // Link to Managed HSM from HsmPoolResourceID // Reference: https://learn.microsoft.com/en-us/rest/api/keyvault/keyvault/managed-hsms/get // GET /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/managedHSMs/{name} @@ -243,6 +284,8 @@ func (k keyvaultVaultWrapper) PotentialLinks() map[shared.ItemType]bool { azureshared.NetworkPrivateEndpoint, azureshared.NetworkSubnet, azureshared.KeyVaultManagedHSM, + stdlib.NetworkIP, + stdlib.NetworkHTTP, ) } diff --git a/sources/azure/manual/keyvault-vault_test.go b/sources/azure/manual/keyvault-vault_test.go index 6ab43b1c..2decc20b 100644 --- a/sources/azure/manual/keyvault-vault_test.go +++ b/sources/azure/manual/keyvault-vault_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -18,6 +18,7 @@ import ( 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" ) // mockVaultsPager is a simple mock implementation of VaultsPager @@ -161,6 +162,39 @@ func TestKeyVaultVault(t *testing.T) { 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) @@ -351,6 +385,29 @@ func TestKeyVaultVault(t *testing.T) { t.Error("Expected to find at least one linked item query with a different scope, but all used default scope") } }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockVaultsClient(ctrl) + wrapper := manual.NewKeyVaultVault(mockClient, subscriptionID, resourceGroup) + + links := wrapper.PotentialLinks() + if len(links) == 0 { + t.Error("Expected potential links to be defined") + } + + expectedLinks := map[shared.ItemType]bool{ + azureshared.NetworkPrivateEndpoint: true, + azureshared.NetworkSubnet: true, + azureshared.KeyVaultManagedHSM: true, + stdlib.NetworkIP: true, + stdlib.NetworkHTTP: true, + } + for expectedType, expectedValue := range expectedLinks { + if links[expectedType] != expectedValue { + t.Errorf("Expected PotentialLinks[%s] = %v, got %v", expectedType.String(), expectedValue, links[expectedType]) + } + } + }) } // createAzureKeyVault creates a mock Azure Key Vault with linked resources @@ -381,7 +438,7 @@ func createAzureKeyVault(vaultName, subscriptionID, resourceGroup string) *armke }, }, }, - // Network ACLs with Virtual Network Rules + // Network ACLs with Virtual Network Rules and IP Rules NetworkACLs: &armkeyvault.NetworkRuleSet{ VirtualNetworkRules: []*armkeyvault.VirtualNetworkRule{ { @@ -391,7 +448,13 @@ func createAzureKeyVault(vaultName, subscriptionID, resourceGroup string) *armke ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet-diff-rg/subnets/test-subnet-diff-rg"), }, }, + IPRules: []*armkeyvault.IPRule{ + {Value: to.Ptr("192.168.1.100")}, + {Value: to.Ptr("10.0.0.0/24")}, + }, }, + // Vault URI for keys and secrets operations + VaultURI: to.Ptr("https://" + vaultName + ".vault.azure.net/"), // Managed HSM Pool Resource ID HsmPoolResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/hsm-rg/providers/Microsoft.KeyVault/managedHSMs/test-managed-hsm"), }, diff --git a/sources/azure/manual/links_helpers.go b/sources/azure/manual/links_helpers.go new file mode 100644 index 00000000..40b84657 --- /dev/null +++ b/sources/azure/manual/links_helpers.go @@ -0,0 +1,41 @@ +package manual + +import ( + "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/cli/sources/stdlib" +) + +// appendLinkIfValid appends a linked item query when the value passes validation. +// Skips empty strings and any value in skipValues. If createQuery returns a non-nil query, it is appended. +// Use this for reusable link-creation logic with configurable skip rules (e.g. DNS servers, IP/CIDR prefixes). +func appendLinkIfValid( + queries *[]*sdp.LinkedItemQuery, + value string, + skipValues []string, + createQuery func(string) *sdp.LinkedItemQuery, +) { + if value == "" { + return + } + for _, skip := range skipValues { + if value == skip { + return + } + } + if q := createQuery(value); q != nil { + *queries = append(*queries, q) + } +} + +// networkIPQuery returns a linked item query for stdlib.NetworkIP. +func networkIPQuery(query string) *sdp.LinkedItemQuery { + return &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: query, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + } +} diff --git a/sources/azure/manual/network-application-gateway.go b/sources/azure/manual/network-application-gateway.go index 37f002db..c5955b16 100644 --- a/sources/azure/manual/network-application-gateway.go +++ b/sources/azure/manual/network-application-gateway.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "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" diff --git a/sources/azure/manual/network-application-gateway_test.go b/sources/azure/manual/network-application-gateway_test.go index b3755fb8..97de1bbe 100644 --- a/sources/azure/manual/network-application-gateway_test.go +++ b/sources/azure/manual/network-application-gateway_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/manual/network-load-balancer.go b/sources/azure/manual/network-load-balancer.go index a9d0e7f8..6f3789ce 100644 --- a/sources/azure/manual/network-load-balancer.go +++ b/sources/azure/manual/network-load-balancer.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -170,23 +170,69 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm // Subnet ID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnet}/subnets/{subnet} parts := strings.Split(strings.Trim(subnetID, "/"), "/") if len(parts) >= 10 && parts[0] == "subscriptions" && parts[2] == "resourceGroups" && parts[4] == "providers" && parts[5] == "Microsoft.Network" && parts[6] == "virtualNetworks" && parts[8] == "subnets" { - subscriptionID := parts[1] - resourceGroup := parts[3] vnetName := parts[7] subnetName := parts[9] - scope := fmt.Sprintf("%s.%s", subscriptionID, resourceGroup) + linkedScope := fmt.Sprintf("%s.%s", parts[1], parts[3]) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ Type: azureshared.NetworkSubnet.String(), - Method: sdp.QueryMethod_GET, // Field is an ID, so use GET with composite lookup key + Method: sdp.QueryMethod_GET, Query: shared.CompositeLookupKey(vnetName, subnetName), - Scope: scope, + 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 - }, // Subnet provides the network location for the load balancer's frontend + }, + }) + } + } + + // Link to Gateway Load Balancer frontend IP if referenced (e.g. LB chained to Gateway LB) + // Reference: https://learn.microsoft.com/en-us/rest/api/load-balancer/load-balancer-frontend-ip-configurations/get?view=rest-load-balancer-2025-03-01&tabs=HTTP + if frontendIPConfig.Properties.GatewayLoadBalancer != nil && frontendIPConfig.Properties.GatewayLoadBalancer.ID != nil { + params := azureshared.ExtractPathParamsFromResourceID(*frontendIPConfig.Properties.GatewayLoadBalancer.ID, []string{"loadBalancers", "frontendIPConfigurations"}) + if len(params) >= 2 { + linkedScope := azureshared.ExtractScopeFromResourceID(*frontendIPConfig.Properties.GatewayLoadBalancer.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + + // Link to Public IP Prefix if referenced + // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/public-ip-prefixes/get?view=rest-virtualnetwork-2025-03-01&tabs=HTTP + if frontendIPConfig.Properties.PublicIPPrefix != nil && frontendIPConfig.Properties.PublicIPPrefix.ID != nil { + publicIPPrefixName := azureshared.ExtractResourceName(*frontendIPConfig.Properties.PublicIPPrefix.ID) + if publicIPPrefixName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*frontendIPConfig.Properties.PublicIPPrefix.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPublicIPPrefix.String(), + Method: sdp.QueryMethod_GET, + 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 + }, }) } } @@ -195,7 +241,7 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm if frontendIPConfig.Properties.PrivateIPAddress != nil && *frontendIPConfig.Properties.PrivateIPAddress != "" { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: "ip", + Type: stdlib.NetworkIP.String(), Method: sdp.QueryMethod_GET, Query: *frontendIPConfig.Properties.PrivateIPAddress, Scope: "global", @@ -227,9 +273,123 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm BlastPropagation: &sdp.BlastPropagation{ In: true, // BackendAddressPool changes affect which backends receive traffic Out: true, // Load balancer changes (like deletion) affect the backend address pool - }, // BackendAddressPool is a child resource of the Load Balancer; bidirectional dependency + }, }) } + + // Link to Virtual Network if backend pool references one + // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/virtual-networks/get?view=rest-virtualnetwork-2025-03-01&tabs=HTTP + if backendPool.Properties != nil && backendPool.Properties.VirtualNetwork != nil && backendPool.Properties.VirtualNetwork.ID != nil { + vnetName := azureshared.ExtractResourceName(*backendPool.Properties.VirtualNetwork.ID) + if vnetName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*backendPool.Properties.VirtualNetwork.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + + // Link from backend addresses (LoadBalancerBackendAddress) to frontend IP config, subnet, VNet, and IP + if backendPool.Properties != nil && backendPool.Properties.LoadBalancerBackendAddresses != nil { + for _, addr := range backendPool.Properties.LoadBalancerBackendAddresses { + if addr == nil || addr.Properties == nil { + continue + } + // Link to Frontend IP Configuration (regional LB) if referenced + if addr.Properties.LoadBalancerFrontendIPConfiguration != nil && addr.Properties.LoadBalancerFrontendIPConfiguration.ID != nil { + params := azureshared.ExtractPathParamsFromResourceID(*addr.Properties.LoadBalancerFrontendIPConfiguration.ID, []string{"loadBalancers", "frontendIPConfigurations"}) + if len(params) >= 2 { + linkedScope := azureshared.ExtractScopeFromResourceID(*addr.Properties.LoadBalancerFrontendIPConfiguration.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(params[0], params[1]), + Scope: linkedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + } + // Link to Subnet if referenced + if addr.Properties.Subnet != nil && addr.Properties.Subnet.ID != nil { + params := azureshared.ExtractPathParamsFromResourceID(*addr.Properties.Subnet.ID, []string{"virtualNetworks", "subnets"}) + if len(params) >= 2 { + linkedScope := azureshared.ExtractScopeFromResourceID(*addr.Properties.Subnet.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(params[0], params[1]), + Scope: linkedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + // Link to Virtual Network if referenced + if addr.Properties.VirtualNetwork != nil && addr.Properties.VirtualNetwork.ID != nil { + vnetName := azureshared.ExtractResourceName(*addr.Properties.VirtualNetwork.ID) + if vnetName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*addr.Properties.VirtualNetwork.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: vnetName, + Scope: linkedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + // Link to stdlib IP if backend address has IP + if addr.Properties.IPAddress != nil && *addr.Properties.IPAddress != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *addr.Properties.IPAddress, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + } + } } } @@ -392,7 +552,9 @@ func (n networkLoadBalancerWrapper) PotentialLinks() map[shared.ItemType]bool { azureshared.NetworkLoadBalancerInboundNatPool: true, // External resources azureshared.NetworkPublicIPAddress: true, + azureshared.NetworkPublicIPPrefix: true, azureshared.NetworkSubnet: true, + azureshared.NetworkVirtualNetwork: true, azureshared.NetworkNetworkInterface: true, // Standard library resources stdlib.NetworkIP: true, diff --git a/sources/azure/manual/network-load-balancer_test.go b/sources/azure/manual/network-load-balancer_test.go index 8e958da4..f4bda115 100644 --- a/sources/azure/manual/network-load-balancer_test.go +++ b/sources/azure/manual/network-load-balancer_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -403,6 +403,12 @@ func TestNetworkLoadBalancer(t *testing.T) { if !potentialLinks[azureshared.NetworkNetworkInterface] { t.Error("Expected PotentialLinks to include NetworkNetworkInterface") } + if !potentialLinks[azureshared.NetworkPublicIPPrefix] { + t.Error("Expected PotentialLinks to include NetworkPublicIPPrefix") + } + if !potentialLinks[azureshared.NetworkVirtualNetwork] { + t.Error("Expected PotentialLinks to include NetworkVirtualNetwork") + } // Verify TerraformMappings mappings := w.TerraformMappings() diff --git a/sources/azure/manual/network-network-interface.go b/sources/azure/manual/network-network-interface.go index 2867e0f0..273740a5 100644 --- a/sources/azure/manual/network-network-interface.go +++ b/sources/azure/manual/network-network-interface.go @@ -4,12 +4,13 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/overmindtech/cli/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 NetworkNetworkInterfaceLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkNetworkInterface) @@ -131,6 +132,365 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn } } + // Private endpoint (read-only reference when NIC is used by a private endpoint) + if networkInterface.Properties != nil && networkInterface.Properties.PrivateEndpoint != nil && + networkInterface.Properties.PrivateEndpoint.ID != nil { + peName := azureshared.ExtractResourceName(*networkInterface.Properties.PrivateEndpoint.ID) + if peName != "" { + scope := azureshared.ExtractScopeFromResourceID(*networkInterface.Properties.PrivateEndpoint.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + + // Private Link Service (when this NIC is the frontend of a private link service) + if networkInterface.Properties != nil && networkInterface.Properties.PrivateLinkService != nil && + networkInterface.Properties.PrivateLinkService.ID != nil { + plsName := azureshared.ExtractResourceName(*networkInterface.Properties.PrivateLinkService.ID) + if plsName != "" { + scope := azureshared.ExtractScopeFromResourceID(*networkInterface.Properties.PrivateLinkService.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateLinkService.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + + // DSCP configuration (read-only reference) + if networkInterface.Properties != nil && networkInterface.Properties.DscpConfiguration != nil && + networkInterface.Properties.DscpConfiguration.ID != nil { + dscpName := azureshared.ExtractResourceName(*networkInterface.Properties.DscpConfiguration.ID) + if dscpName != "" { + scope := azureshared.ExtractScopeFromResourceID(*networkInterface.Properties.DscpConfiguration.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkDscpConfiguration.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + + // Tap configurations (child resource; list by NIC name) + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkNetworkInterfaceTapConfiguration.String(), + Method: sdp.QueryMethod_SEARCH, + 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 + if networkInterface.Properties != nil && networkInterface.Properties.IPConfigurations != nil { + for _, ipConfig := range networkInterface.Properties.IPConfigurations { + if ipConfig == nil || ipConfig.Properties == nil { + continue + } + props := ipConfig.Properties + + // Subnet + if props.Subnet != nil && props.Subnet.ID != nil { + subnetParams := azureshared.ExtractPathParamsFromResourceID(*props.Subnet.ID, []string{"virtualNetworks", "subnets"}) + if len(subnetParams) >= 2 { + vnetName, subnetName := subnetParams[0], subnetParams[1] + scope := azureshared.ExtractScopeFromResourceID(*props.Subnet.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + + // Public IP address + if props.PublicIPAddress != nil && props.PublicIPAddress.ID != nil { + pipName := azureshared.ExtractResourceName(*props.PublicIPAddress.ID) + if pipName != "" { + scope := azureshared.ExtractScopeFromResourceID(*props.PublicIPAddress.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPublicIPAddress.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + + // Private IP address -> stdlib ip + if props.PrivateIPAddress != nil && *props.PrivateIPAddress != "" { + addr := *props.PrivateIPAddress + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: addr, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + + // Application security groups + if props.ApplicationSecurityGroups != nil { + for _, asg := range props.ApplicationSecurityGroups { + if asg != nil && asg.ID != nil { + asgName := azureshared.ExtractResourceName(*asg.ID) + if asgName != "" { + scope := azureshared.ExtractScopeFromResourceID(*asg.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkApplicationSecurityGroup.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + } + } + + // Load balancer backend address pools + if props.LoadBalancerBackendAddressPools != nil { + for _, pool := range props.LoadBalancerBackendAddressPools { + if pool != nil && pool.ID != nil { + params := azureshared.ExtractPathParamsFromResourceID(*pool.ID, []string{"loadBalancers", "backendAddressPools"}) + if len(params) >= 2 { + scope := azureshared.ExtractScopeFromResourceID(*pool.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkLoadBalancerBackendAddressPool.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + } + } + + // Load balancer inbound NAT rules + if props.LoadBalancerInboundNatRules != nil { + for _, rule := range props.LoadBalancerInboundNatRules { + if rule != nil && rule.ID != nil { + params := azureshared.ExtractPathParamsFromResourceID(*rule.ID, []string{"loadBalancers", "inboundNatRules"}) + if len(params) >= 2 { + scope := azureshared.ExtractScopeFromResourceID(*rule.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkLoadBalancerInboundNatRule.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + } + } + + // Application Gateway backend address pools + if props.ApplicationGatewayBackendAddressPools != nil { + for _, pool := range props.ApplicationGatewayBackendAddressPools { + if pool != nil && pool.ID != nil { + params := azureshared.ExtractPathParamsFromResourceID(*pool.ID, []string{"applicationGateways", "backendAddressPools"}) + if len(params) >= 2 { + scope := azureshared.ExtractScopeFromResourceID(*pool.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkApplicationGatewayBackendAddressPool.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + } + } + + // Gateway Load Balancer (frontend IP config reference) + if props.GatewayLoadBalancer != nil && props.GatewayLoadBalancer.ID != nil { + params := azureshared.ExtractPathParamsFromResourceID(*props.GatewayLoadBalancer.ID, []string{"loadBalancers", "frontendIPConfigurations"}) + if len(params) >= 2 { + scope := azureshared.ExtractScopeFromResourceID(*props.GatewayLoadBalancer.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + + // Virtual Network Taps + if props.VirtualNetworkTaps != nil { + for _, tap := range props.VirtualNetworkTaps { + if tap != nil && tap.ID != nil { + tapName := azureshared.ExtractResourceName(*tap.ID) + if tapName != "" { + scope := azureshared.ExtractScopeFromResourceID(*tap.ID) + if scope == "" { + scope = n.DefaultScope() + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetworkTap.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + } + } + } + } + + // DNS settings: link IPs to stdlib.NetworkIP and hostnames to stdlib.NetworkDNS + if networkInterface.Properties != nil && networkInterface.Properties.DNSSettings != nil { + dns := networkInterface.Properties.DNSSettings + if dns.DNSServers != nil { + for _, srv := range dns.DNSServers { + if srv == nil { + continue + } + appendDNSServerLinkIfValid(&sdpItem.LinkedItemQueries, *srv, "AzureProvidedDNS") + } + } + if dns.InternalDNSNameLabel != nil && *dns.InternalDNSNameLabel != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: *dns.InternalDNSNameLabel, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + if dns.InternalFqdn != nil && *dns.InternalFqdn != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: *dns.InternalFqdn, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + } + return sdpItem, nil } @@ -160,10 +520,24 @@ func (n networkNetworkInterfaceWrapper) GetLookups() sources.ItemTypeLookups { func (n networkNetworkInterfaceWrapper) PotentialLinks() map[shared.ItemType]bool { return map[shared.ItemType]bool{ - azureshared.NetworkVirtualNetwork: true, - azureshared.ComputeVirtualMachine: true, - azureshared.NetworkNetworkSecurityGroup: true, - azureshared.NetworkNetworkInterfaceIPConfiguration: true, + azureshared.NetworkVirtualNetwork: true, + azureshared.ComputeVirtualMachine: true, + azureshared.NetworkNetworkSecurityGroup: true, + azureshared.NetworkNetworkInterfaceIPConfiguration: true, + azureshared.NetworkNetworkInterfaceTapConfiguration: true, + azureshared.NetworkSubnet: true, + azureshared.NetworkPublicIPAddress: true, + azureshared.NetworkPrivateEndpoint: true, + azureshared.NetworkPrivateLinkService: true, + azureshared.NetworkDscpConfiguration: true, + azureshared.NetworkApplicationSecurityGroup: true, + azureshared.NetworkLoadBalancerBackendAddressPool: true, + azureshared.NetworkLoadBalancerInboundNatRule: true, + azureshared.NetworkApplicationGatewayBackendAddressPool: true, + azureshared.NetworkLoadBalancerFrontendIPConfiguration: true, + azureshared.NetworkVirtualNetworkTap: true, + stdlib.NetworkIP: true, + stdlib.NetworkDNS: true, } } diff --git a/sources/azure/manual/network-network-interface_test.go b/sources/azure/manual/network-network-interface_test.go index c10f9581..4fc273bf 100644 --- a/sources/azure/manual/network-network-interface_test.go +++ b/sources/azure/manual/network-network-interface_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -18,6 +18,7 @@ import ( 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 TestNetworkNetworkInterface(t *testing.T) { @@ -97,10 +98,103 @@ func TestNetworkNetworkInterface(t *testing.T) { 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) }) + + t.Run("DNSServers_IP_and_hostname", func(t *testing.T) { + nicWithDNS := createAzureNetworkInterfaceWithDNSServers(nicName, "test-vm", "test-nsg", []string{"10.0.0.1", "dns.internal"}) + mockClient := mocks.NewMockNetworkInterfacesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, nicName).Return( + armnetwork.InterfacesClientGetResponse{ + Interface: *nicWithDNS, + }, nil) + + wrapper := manual.NewNetworkNetworkInterface(mockClient, subscriptionID, resourceGroup) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], nicName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + // Same base links as main Get test, plus DNS server links (IP → NetworkIP, hostname → NetworkDNS) + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkNetworkInterfaceIPConfiguration.String(), + 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) + }) }) t.Run("Get_InvalidQueryParts", func(t *testing.T) { @@ -402,3 +496,16 @@ func createAzureNetworkInterface(nicName, vmName, nsgName string) *armnetwork.In }, } } + +// createAzureNetworkInterfaceWithDNSServers creates a mock Azure network interface with DNSSettings for testing DNS server links (IP vs hostname). +func createAzureNetworkInterfaceWithDNSServers(nicName, vmName, nsgName string, dnsServers []string) *armnetwork.Interface { + nic := createAzureNetworkInterface(nicName, vmName, nsgName) + ptrs := make([]*string, len(dnsServers)) + for i := range dnsServers { + ptrs[i] = to.Ptr(dnsServers[i]) + } + nic.Properties.DNSSettings = &armnetwork.InterfaceDNSSettings{ + DNSServers: ptrs, + } + return nic +} diff --git a/sources/azure/manual/network-network-security-group.go b/sources/azure/manual/network-network-security-group.go index a871101f..e742affa 100644 --- a/sources/azure/manual/network-network-security-group.go +++ b/sources/azure/manual/network-network-security-group.go @@ -3,9 +3,10 @@ package manual import ( "context" "errors" + "net" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "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" @@ -13,10 +14,24 @@ import ( "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 NetworkNetworkSecurityGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkNetworkSecurityGroup) +// appendIPOrCIDRLinkIfValid appends a linked item query to stdlib.NetworkIP when the prefix is an IP address or CIDR (not a service tag like VirtualNetwork, Internet, *). +func appendIPOrCIDRLinkIfValid(queries *[]*sdp.LinkedItemQuery, prefix string) { + appendLinkIfValid(queries, prefix, []string{"*"}, func(p string) *sdp.LinkedItemQuery { + if net.ParseIP(p) != nil { + return networkIPQuery(p) + } + if _, _, err := net.ParseCIDR(p); err == nil { + return networkIPQuery(p) + } + return nil + }) +} + type networkNetworkSecurityGroupWrapper struct { client clients.NetworkSecurityGroupsClient @@ -238,6 +253,40 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n } } + // Link to FlowLogs (external resources) + // Reference: https://learn.microsoft.com/en-us/rest/api/network-watcher/flow-logs/get + if networkSecurityGroup.Properties != nil && networkSecurityGroup.Properties.FlowLogs != nil { + for _, flowLogRef := range networkSecurityGroup.Properties.FlowLogs { + if flowLogRef != nil && flowLogRef.ID != nil && *flowLogRef.ID != "" { + flowLogID := *flowLogRef.ID + params := azureshared.ExtractPathParamsFromResourceID(flowLogID, []string{"networkWatchers", "flowLogs"}) + if len(params) < 2 { + params = azureshared.ExtractPathParamsFromResourceID(flowLogID, []string{"networkWatchers", "FlowLogs"}) + } + if len(params) >= 2 { + networkWatcherName := params[0] + flowLogName := params[1] + scope := n.DefaultScope() + if extractedScope := azureshared.ExtractScopeFromResourceID(flowLogID); extractedScope != "" { + scope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkFlowLog.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + } + } + } + // Link to ApplicationSecurityGroups and IPGroups from SecurityRules // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/application-security-groups/get // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/ip-groups/get @@ -302,8 +351,23 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n } } - // Note: IPGroups (SourceIPGroups/DestinationIPGroups) are not available in the current Azure SDK version - // These fields may be available in future SDK versions or may require a different SDK package + // Link to stdlib.NetworkIP for source/destination address prefixes when they are IPs or CIDRs + if securityRule.Properties.SourceAddressPrefix != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *securityRule.Properties.SourceAddressPrefix) + } + for _, p := range securityRule.Properties.SourceAddressPrefixes { + if p != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *p) + } + } + if securityRule.Properties.DestinationAddressPrefix != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *securityRule.Properties.DestinationAddressPrefix) + } + for _, p := range securityRule.Properties.DestinationAddressPrefixes { + if p != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *p) + } + } } } } @@ -368,8 +432,23 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n } } - // Note: IPGroups (SourceIPGroups/DestinationIPGroups) are not available in the current Azure SDK version - // These fields may be available in future SDK versions or may require a different SDK package + // Link to stdlib.NetworkIP for source/destination address prefixes when they are IPs or CIDRs + if defaultSecurityRule.Properties.SourceAddressPrefix != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *defaultSecurityRule.Properties.SourceAddressPrefix) + } + for _, p := range defaultSecurityRule.Properties.SourceAddressPrefixes { + if p != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *p) + } + } + if defaultSecurityRule.Properties.DestinationAddressPrefix != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *defaultSecurityRule.Properties.DestinationAddressPrefix) + } + for _, p := range defaultSecurityRule.Properties.DestinationAddressPrefixes { + if p != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *p) + } + } } } } @@ -390,8 +469,10 @@ func (n networkNetworkSecurityGroupWrapper) PotentialLinks() map[shared.ItemType azureshared.NetworkDefaultSecurityRule: true, azureshared.NetworkSubnet: true, azureshared.NetworkNetworkInterface: true, + azureshared.NetworkFlowLog: true, azureshared.NetworkApplicationSecurityGroup: true, azureshared.NetworkIPGroup: true, + stdlib.NetworkIP: true, } } diff --git a/sources/azure/manual/network-network-security-group_test.go b/sources/azure/manual/network-network-security-group_test.go index 8d0f5c72..84f09695 100644 --- a/sources/azure/manual/network-network-security-group_test.go +++ b/sources/azure/manual/network-network-security-group_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -18,6 +18,7 @@ import ( 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 TestNetworkNetworkSecurityGroup(t *testing.T) { @@ -441,8 +442,10 @@ func TestNetworkNetworkSecurityGroup(t *testing.T) { azureshared.NetworkDefaultSecurityRule, azureshared.NetworkSubnet, azureshared.NetworkNetworkInterface, + azureshared.NetworkFlowLog, azureshared.NetworkApplicationSecurityGroup, azureshared.NetworkIPGroup, + stdlib.NetworkIP, } for _, expectedLink := range expectedLinks { if !potentialLinks[expectedLink] { diff --git a/sources/azure/manual/network-public-ip-address.go b/sources/azure/manual/network-public-ip-address.go index b17ff72c..a4128d15 100644 --- a/sources/azure/manual/network-public-ip-address.go +++ b/sources/azure/manual/network-public-ip-address.go @@ -5,7 +5,7 @@ import ( "errors" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" diff --git a/sources/azure/manual/network-public-ip-address_test.go b/sources/azure/manual/network-public-ip-address_test.go index 605a29f8..2bf7c664 100644 --- a/sources/azure/manual/network-public-ip-address_test.go +++ b/sources/azure/manual/network-public-ip-address_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/manual/network-route-table.go b/sources/azure/manual/network-route-table.go index 97e67b36..644bb2a4 100644 --- a/sources/azure/manual/network-route-table.go +++ b/sources/azure/manual/network-route-table.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -193,7 +193,6 @@ func (n networkRouteTableWrapper) PotentialLinks() map[shared.ItemType]bool { return shared.NewItemTypesSet( azureshared.NetworkRoute, azureshared.NetworkSubnet, - azureshared.NetworkVirtualNetworkGateway, stdlib.NetworkIP, ) } diff --git a/sources/azure/manual/network-route-table_test.go b/sources/azure/manual/network-route-table_test.go index 46c45668..e6dc9323 100644 --- a/sources/azure/manual/network-route-table_test.go +++ b/sources/azure/manual/network-route-table_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -470,7 +470,6 @@ func TestNetworkRouteTable(t *testing.T) { expectedLinks := []shared.ItemType{ azureshared.NetworkRoute, azureshared.NetworkSubnet, - azureshared.NetworkVirtualNetworkGateway, stdlib.NetworkIP, } for _, expectedLink := range expectedLinks { diff --git a/sources/azure/manual/network-virtual-network.go b/sources/azure/manual/network-virtual-network.go index a31b30dc..61580796 100644 --- a/sources/azure/manual/network-virtual-network.go +++ b/sources/azure/manual/network-virtual-network.go @@ -4,12 +4,13 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "github.com/overmindtech/cli/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 NetworkVirtualNetworkLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkVirtualNetwork) @@ -301,6 +302,42 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn } } + // Link to default public NAT Gateway (VNet-level) + // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/nat-gateways/get + if network.Properties != nil && network.Properties.DefaultPublicNatGateway != nil && network.Properties.DefaultPublicNatGateway.ID != nil { + natGatewayID := *network.Properties.DefaultPublicNatGateway.ID + natGatewayName := azureshared.ExtractResourceName(natGatewayID) + if natGatewayName != "" { + scope := n.DefaultScope() + if extractedScope := azureshared.ExtractScopeFromResourceID(natGatewayID); extractedScope != "" { + scope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkNatGateway.String(), + Method: sdp.QueryMethod_GET, + 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) + }, + }) + } + } + + // Link DHCP DNS servers to stdlib ip (IP addresses) or stdlib dns (hostnames) + // Reference: DhcpOptions contains DNS servers available to VMs in the VNet + if network.Properties != nil && network.Properties.DhcpOptions != nil && network.Properties.DhcpOptions.DNSServers != nil { + for _, dnsServerPtr := range network.Properties.DhcpOptions.DNSServers { + if dnsServerPtr == nil { + continue + } + appendDNSServerLinkIfValid(&sdpItem.LinkedItemQueries, *dnsServerPtr, "AzureProvidedDNS") + } + } + return sdpItem, nil } @@ -320,6 +357,8 @@ func (n networkVirtualNetworkWrapper) PotentialLinks() map[shared.ItemType]bool azureshared.NetworkRouteTable, azureshared.NetworkPrivateEndpoint, azureshared.NetworkVirtualNetwork, + stdlib.NetworkIP, + stdlib.NetworkDNS, ) } diff --git a/sources/azure/manual/network-virtual-network_test.go b/sources/azure/manual/network-virtual-network_test.go index 341c05db..b7c5bf15 100644 --- a/sources/azure/manual/network-virtual-network_test.go +++ b/sources/azure/manual/network-virtual-network_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -18,6 +18,7 @@ import ( 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 TestNetworkVirtualNetwork(t *testing.T) { @@ -92,6 +93,81 @@ func TestNetworkVirtualNetwork(t *testing.T) { }) }) + t.Run("Get_WithDefaultPublicNatGatewayAndDhcpOptions", func(t *testing.T) { + vnetName := "test-vnet-with-links" + vnet := createAzureVirtualNetworkWithDefaultNatGatewayAndDhcpOptions(vnetName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockVirtualNetworksClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, vnetName, nil).Return( + armnetwork.VirtualNetworksClientGetResponse{ + VirtualNetwork: *vnet, + }, nil) + + wrapper := manual.NewNetworkVirtualNetwork(mockClient, subscriptionID, resourceGroup) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], vnetName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkSubnet.String(), + 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) + }) + }) + t.Run("Get_InvalidQueryParts", func(t *testing.T) { mockClient := mocks.NewMockVirtualNetworksClient(ctrl) @@ -294,6 +370,12 @@ func TestNetworkVirtualNetwork(t *testing.T) { if !potentialLinks[azureshared.NetworkVirtualNetworkPeering] { t.Error("Expected PotentialLinks to include NetworkVirtualNetworkPeering") } + if !potentialLinks[stdlib.NetworkIP] { + t.Error("Expected PotentialLinks to include stdlib.NetworkIP") + } + if !potentialLinks[stdlib.NetworkDNS] { + t.Error("Expected PotentialLinks to include stdlib.NetworkDNS") + } // Verify TerraformMappings mappings := w.TerraformMappings() @@ -384,3 +466,38 @@ func createAzureVirtualNetwork(vnetName string) *armnetwork.VirtualNetwork { }, } } + +// createAzureVirtualNetworkWithDefaultNatGatewayAndDhcpOptions creates a VNet with +// DefaultPublicNatGateway and DhcpOptions.DNSServers (IP and hostname) for testing linked queries. +func createAzureVirtualNetworkWithDefaultNatGatewayAndDhcpOptions(vnetName, subscriptionID, resourceGroup string) *armnetwork.VirtualNetwork { + natGatewayID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/natGateways/test-nat-gateway" + return &armnetwork.VirtualNetwork{ + Name: to.Ptr(vnetName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armnetwork.VirtualNetworkPropertiesFormat{ + AddressSpace: &armnetwork.AddressSpace{ + AddressPrefixes: []*string{to.Ptr("10.0.0.0/16")}, + }, + DefaultPublicNatGateway: &armnetwork.SubResource{ + ID: to.Ptr(natGatewayID), + }, + DhcpOptions: &armnetwork.DhcpOptions{ + DNSServers: []*string{ + to.Ptr("10.0.0.1"), // IP address → stdlib.NetworkIP + to.Ptr("dns.internal"), // hostname → stdlib.NetworkDNS + }, + }, + Subnets: []*armnetwork.Subnet{ + { + Name: to.Ptr("default"), + Properties: &armnetwork.SubnetPropertiesFormat{ + AddressPrefix: to.Ptr("10.0.0.0/24"), + }, + }, + }, + }, + } +} diff --git a/sources/azure/manual/sql-database.go b/sources/azure/manual/sql-database.go index bfe8a2ee..d073230f 100644 --- a/sources/azure/manual/sql-database.go +++ b/sources/azure/manual/sql-database.go @@ -2,8 +2,9 @@ package manual import ( "context" + "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -233,6 +234,156 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, } } + if database.Properties != nil && database.Properties.FailoverGroupID != nil { + // FailoverGroupID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/failoverGroups/{failoverGroupName} + params := azureshared.ExtractPathParamsFromResourceID(*database.Properties.FailoverGroupID, []string{"servers", "failoverGroups"}) + if len(params) >= 2 { + failoverServerName := params[0] + failoverGroupName := params[1] + linkedScope := azureshared.ExtractScopeFromResourceID(*database.Properties.FailoverGroupID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLServerFailoverGroup.String(), + Method: sdp.QueryMethod_GET, + 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 + }) + } + } + + if database.Properties != nil && database.Properties.LongTermRetentionBackupResourceID != nil { + locationName, ltrServerName, ltrDatabaseName, backupName := azureshared.ExtractSQLLongTermRetentionBackupInfoFromResourceID(*database.Properties.LongTermRetentionBackupResourceID) + if locationName != "" && ltrServerName != "" && ltrDatabaseName != "" && backupName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*database.Properties.LongTermRetentionBackupResourceID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLLongTermRetentionBackup.String(), + Method: sdp.QueryMethod_GET, + 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 + }) + } + } + + if database.Properties != nil && database.Properties.MaintenanceConfigurationID != nil && *database.Properties.MaintenanceConfigurationID != "" { + configName := azureshared.ExtractResourceName(*database.Properties.MaintenanceConfigurationID) + if configName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*database.Properties.MaintenanceConfigurationID) + if linkedScope == "" && strings.Contains(*database.Properties.MaintenanceConfigurationID, "publicMaintenanceConfigurations") { + linkedScope = azureshared.ExtractSubscriptionIDFromResourceID(*database.Properties.MaintenanceConfigurationID) + } + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.MaintenanceMaintenanceConfiguration.String(), + Method: sdp.QueryMethod_GET, + 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 + }) + } + } + + // Link Key Vault Keys from EncryptionProtector and Keys map (deduplicate by vaultName+keyName) + seenKeyVaultKeys := make(map[string]bool) + addKeyVaultKeyLink := func(vaultName, keyName string) { + if vaultName == "" || keyName == "" { + return + } + key := vaultName + "|" + keyName + if seenKeyVaultKeys[key] { + return + } + seenKeyVaultKeys[key] = true + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultKey.String(), + Method: sdp.QueryMethod_GET, + 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 != "" { + addKeyVaultKeyLink( + azureshared.ExtractVaultNameFromURI(*database.Properties.EncryptionProtector), + azureshared.ExtractKeyNameFromURI(*database.Properties.EncryptionProtector), + ) + } + if database.Properties != nil && database.Properties.Keys != nil { + for keyURI := range database.Properties.Keys { + addKeyVaultKeyLink( + azureshared.ExtractVaultNameFromURI(keyURI), + azureshared.ExtractKeyNameFromURI(keyURI), + ) + } + } + + if database.Identity != nil && database.Identity.UserAssignedIdentities != nil { + for identityResourceID := range database.Identity.UserAssignedIdentities { + if identityResourceID == "" { + continue + } + identityName := azureshared.ExtractResourceName(identityResourceID) + linkedScope := azureshared.ExtractScopeFromResourceID(identityResourceID) + if identityName != "" && linkedScope != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ManagedIdentityUserAssignedIdentity.String(), + Method: sdp.QueryMethod_GET, + 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 + }) + } + } + } + + // Database Schemas - child resource with LIST endpoint + // GET /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/databases/{databaseName}/schemas + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLDatabaseSchema.String(), + Method: sdp.QueryMethod_SEARCH, + 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 } @@ -291,11 +442,18 @@ func (s sqlDatabaseWrapper) SearchLookups() []sources.ItemTypeLookups { func (s sqlDatabaseWrapper) PotentialLinks() map[shared.ItemType]bool { return map[shared.ItemType]bool{ - azureshared.SQLServer: true, - azureshared.SQLElasticPool: true, - azureshared.SQLRecoverableDatabase: true, - azureshared.SQLRestorableDroppedDatabase: true, - azureshared.SQLRecoveryServicesRecoveryPoint: true, + azureshared.SQLServer: true, + azureshared.SQLDatabase: true, // source database / copy source + azureshared.SQLElasticPool: true, + azureshared.SQLRecoverableDatabase: true, + azureshared.SQLRestorableDroppedDatabase: true, + azureshared.SQLRecoveryServicesRecoveryPoint: true, + azureshared.SQLServerFailoverGroup: true, + azureshared.SQLLongTermRetentionBackup: true, + azureshared.MaintenanceMaintenanceConfiguration: true, + azureshared.KeyVaultKey: true, + azureshared.ManagedIdentityUserAssignedIdentity: true, + azureshared.SQLDatabaseSchema: true, } } diff --git a/sources/azure/manual/sql-database_test.go b/sources/azure/manual/sql-database_test.go index 61066fe0..48a61728 100644 --- a/sources/azure/manual/sql-database_test.go +++ b/sources/azure/manual/sql-database_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -125,6 +125,17 @@ func TestSqlDatabase(t *testing.T) { 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) @@ -175,6 +186,17 @@ func TestSqlDatabase(t *testing.T) { 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 3a8e806a..aea07b36 100644 --- a/sources/azure/manual/sql-server.go +++ b/sources/azure/manual/sql-server.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + "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" @@ -580,6 +580,22 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s 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) + // Reference: https://learn.microsoft.com/en-us/rest/api/sql/2021-11-01/long-term-retention-backups/list-by-server + // GET /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Sql/servers/{serverName}/longTermRetentionBackups + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLLongTermRetentionBackup.String(), + Method: sdp.QueryMethod_SEARCH, + 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 + }) } // External resources - extracted from IDs in the server response @@ -730,6 +746,7 @@ func (s sqlServerWrapper) PotentialLinks() map[shared.ItemType]bool { azureshared.SQLServerTrustGroup, azureshared.SQLServerOutboundFirewallRule, azureshared.SQLServerPrivateLinkResource, + azureshared.SQLLongTermRetentionBackup, // External resources azureshared.ManagedIdentityUserAssignedIdentity, azureshared.NetworkPrivateEndpoint, diff --git a/sources/azure/manual/sql-server_test.go b/sources/azure/manual/sql-server_test.go index b8873865..a04e58ef 100644 --- a/sources/azure/manual/sql-server_test.go +++ b/sources/azure/manual/sql-server_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -373,6 +373,16 @@ func TestSqlServer(t *testing.T) { 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(), diff --git a/sources/azure/manual/storage-account.go b/sources/azure/manual/storage-account.go index 88cecf6e..f17ff6fa 100644 --- a/sources/azure/manual/storage-account.go +++ b/sources/azure/manual/storage-account.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -160,6 +160,24 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. }, }) + // Link to Private Endpoint Connections (child resource) + // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/private-endpoint-connections/list?view=rest-storagerp-2025-06-01 + // Private endpoint connections can be listed using the storage account name + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StoragePrivateEndpointConnection.String(), + Method: sdp.QueryMethod_SEARCH, + 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) // Reference: https://learn.microsoft.com/en-us/rest/api/managedidentity/user-assigned-identities/get?view=rest-managedidentity-2024-11-30&tabs=HTTP if account.Identity != nil && account.Identity.UserAssignedIdentities != nil { @@ -470,10 +488,11 @@ func (s storageAccountWrapper) GetLookups() sources.ItemTypeLookups { func (s storageAccountWrapper) PotentialLinks() map[shared.ItemType]bool { return map[shared.ItemType]bool{ // Child resources - azureshared.StorageBlobContainer: true, - azureshared.StorageFileShare: true, - azureshared.StorageTable: true, - azureshared.StorageQueue: true, + azureshared.StorageBlobContainer: true, + azureshared.StorageFileShare: true, + azureshared.StorageTable: true, + azureshared.StorageQueue: true, + azureshared.StoragePrivateEndpointConnection: true, // External resources azureshared.ManagedIdentityUserAssignedIdentity: true, azureshared.KeyVaultVault: true, diff --git a/sources/azure/manual/storage-account_test.go b/sources/azure/manual/storage-account_test.go index 1f94f4a6..02696cca 100644 --- a/sources/azure/manual/storage-account_test.go +++ b/sources/azure/manual/storage-account_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -107,6 +107,17 @@ func TestStorageAccount(t *testing.T) { 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", diff --git a/sources/azure/manual/storage-blob-container.go b/sources/azure/manual/storage-blob-container.go index 79c74895..a70b7b6b 100644 --- a/sources/azure/manual/storage-blob-container.go +++ b/sources/azure/manual/storage-blob-container.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -127,6 +127,7 @@ func (s storageBlobContainerWrapper) SearchLookups() []sources.ItemTypeLookups { func (s storageBlobContainerWrapper) PotentialLinks() map[shared.ItemType]bool { return shared.NewItemTypesSet( azureshared.StorageAccount, + azureshared.StorageEncryptionScope, stdlib.NetworkHTTP, stdlib.NetworkDNS, ) @@ -199,6 +200,22 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms }) } + // Link to Storage Encryption Scope when container uses a default encryption scope + if container.ContainerProperties != nil && container.ContainerProperties.DefaultEncryptionScope != nil && *container.ContainerProperties.DefaultEncryptionScope != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageEncryptionScope.String(), + Method: sdp.QueryMethod_GET, + 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 + }, + }) + } + return sdpItem, nil } diff --git a/sources/azure/manual/storage-blob-container_test.go b/sources/azure/manual/storage-blob-container_test.go index cd3bf441..9e1e7316 100644 --- a/sources/azure/manual/storage-blob-container_test.go +++ b/sources/azure/manual/storage-blob-container_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" @@ -175,6 +175,58 @@ func TestStorageBlobContainer(t *testing.T) { }) }) + t.Run("Get_WithDefaultEncryptionScope", func(t *testing.T) { + container := createAzureBlobContainerWithEncryptionScope(containerName, "test-encryption-scope") + + mockClient := mocks.NewMockBlobContainersClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, storageAccountName, containerName).Return( + armstorage.BlobContainersClientGetResponse{ + BlobContainer: *container, + }, nil) + + testClient := &testBlobContainersClient{MockBlobContainersClient: mockClient} + wrapper := manual.NewStorageBlobContainer(testClient, subscriptionID, resourceGroup) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := storageAccountName + shared.QuerySeparator + containerName + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + if len(linkedQueries) != 4 { + t.Fatalf("Expected 4 linked queries (StorageAccount, DNS, HTTP, EncryptionScope), got: %d", len(linkedQueries)) + } + + var hasEncryptionScopeLink bool + for _, linkedQuery := range linkedQueries { + if linkedQuery.GetQuery().GetType() == azureshared.StorageEncryptionScope.String() { + hasEncryptionScopeLink = true + if linkedQuery.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected StorageEncryptionScope linked query method GET, got %s", linkedQuery.GetQuery().GetMethod()) + } + expectedQuery := shared.CompositeLookupKey(storageAccountName, "test-encryption-scope") + if linkedQuery.GetQuery().GetQuery() != expectedQuery { + t.Errorf("Expected StorageEncryptionScope linked query %s, got %s", expectedQuery, linkedQuery.GetQuery().GetQuery()) + } + 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 + } + } + if !hasEncryptionScopeLink { + t.Error("Expected StorageEncryptionScope linked query when DefaultEncryptionScope is set, but didn't find one") + } + }) + t.Run("Get_InvalidQueryParts", func(t *testing.T) { mockClient := mocks.NewMockBlobContainersClient(ctrl) testClient := &testBlobContainersClient{MockBlobContainersClient: mockClient} @@ -380,3 +432,18 @@ func createAzureBlobContainer(containerName string) *armstorage.BlobContainer { Etag: to.Ptr("\"0x8D1234567890ABC\""), } } + +// createAzureBlobContainerWithEncryptionScope creates a mock Azure blob container with a default encryption scope +func createAzureBlobContainerWithEncryptionScope(containerName, encryptionScopeName string) *armstorage.BlobContainer { + return &armstorage.BlobContainer{ + ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/blobServices/default/containers/" + containerName), + Name: to.Ptr(containerName), + Type: to.Ptr("Microsoft.Storage/storageAccounts/blobServices/containers"), + ContainerProperties: &armstorage.ContainerProperties{ + PublicAccess: to.Ptr(armstorage.PublicAccessNone), + DefaultEncryptionScope: to.Ptr(encryptionScopeName), + DenyEncryptionScopeOverride: to.Ptr(false), + }, + Etag: to.Ptr("\"0x8D1234567890ABC\""), + } +} diff --git a/sources/azure/manual/storage-fileshare.go b/sources/azure/manual/storage-fileshare.go index 3e64f5c9..088b0888 100644 --- a/sources/azure/manual/storage-fileshare.go +++ b/sources/azure/manual/storage-fileshare.go @@ -3,7 +3,7 @@ package manual import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -156,6 +156,8 @@ func (s storageFileShareWrapper) azureFileShareToSDPItem(fileShare *armstorage.F 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, }, diff --git a/sources/azure/manual/storage-fileshare_test.go b/sources/azure/manual/storage-fileshare_test.go index a2ad4fe1..a2a42557 100644 --- a/sources/azure/manual/storage-fileshare_test.go +++ b/sources/azure/manual/storage-fileshare_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/manual/storage-queues.go b/sources/azure/manual/storage-queues.go index 26a5f997..e339c2fa 100644 --- a/sources/azure/manual/storage-queues.go +++ b/sources/azure/manual/storage-queues.go @@ -3,7 +3,7 @@ package manual import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -73,6 +73,7 @@ func (s storageQueuesWrapper) azureQueueToSDPItem(queue *armstorage.Queue, stora Scope: scope, } + // Queue is a child of the storage account; queue is affected if account changes, account is not affected by queue changes. sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ Type: azureshared.StorageAccount.String(), @@ -81,8 +82,8 @@ func (s storageQueuesWrapper) azureQueueToSDPItem(queue *armstorage.Queue, stora Scope: scope, }, BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, + 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. }, }) diff --git a/sources/azure/manual/storage-queues_test.go b/sources/azure/manual/storage-queues_test.go index 332ea12d..49973f99 100644 --- a/sources/azure/manual/storage-queues_test.go +++ b/sources/azure/manual/storage-queues_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/manual/storage-table.go b/sources/azure/manual/storage-table.go index f8910762..ca1039ff 100644 --- a/sources/azure/manual/storage-table.go +++ b/sources/azure/manual/storage-table.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -79,6 +79,7 @@ func (s storageTablesWrapper) azureTableToSDPItem(table *armstorage.Table, stora Scope: scope, } + // Link to parent Storage Account (table is a child under tableServices/default/tables). sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ Type: azureshared.StorageAccount.String(), @@ -87,8 +88,8 @@ func (s storageTablesWrapper) azureTableToSDPItem(table *armstorage.Table, stora Scope: scope, }, BlastPropagation: &sdp.BlastPropagation{ - In: true, // Tables ARE affected if storage account changes/deletes - Out: false, // Tables changes/deletes don't affect storage account + In: true, // Table is affected if parent storage account changes or is deleted + Out: false, // Table changes/deletes do not affect the storage account }, }) diff --git a/sources/azure/manual/storage-table_test.go b/sources/azure/manual/storage-table_test.go index 956b67c8..b0bc04c3 100644 --- a/sources/azure/manual/storage-table_test.go +++ b/sources/azure/manual/storage-table_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/discovery" diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index da101933..1cdd82a3 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -70,13 +70,20 @@ var ( NetworkZone = shared.NewItemType(Azure, Network, Zone) NetworkDNSRecordSet = shared.NewItemType(Azure, Network, DNSRecordSet) NetworkDNSVirtualNetworkLink = shared.NewItemType(Azure, Network, DNSVirtualNetworkLink) + NetworkFlowLog = shared.NewItemType(Azure, Network, FlowLog) + NetworkPrivateLinkService = shared.NewItemType(Azure, Network, PrivateLinkService) + NetworkDscpConfiguration = shared.NewItemType(Azure, Network, DscpConfiguration) + NetworkVirtualNetworkTap = shared.NewItemType(Azure, Network, VirtualNetworkTap) + NetworkNetworkInterfaceTapConfiguration = shared.NewItemType(Azure, Network, NetworkInterfaceTapConfiguration) //Storage item types - StorageAccount = shared.NewItemType(Azure, Storage, Account) - StorageBlobContainer = shared.NewItemType(Azure, Storage, BlobContainer) - StorageFileShare = shared.NewItemType(Azure, Storage, FileShare) - StorageTable = shared.NewItemType(Azure, Storage, Table) - StorageQueue = shared.NewItemType(Azure, Storage, Queue) + StorageAccount = shared.NewItemType(Azure, Storage, Account) + StorageBlobContainer = shared.NewItemType(Azure, Storage, BlobContainer) + StorageEncryptionScope = shared.NewItemType(Azure, Storage, EncryptionScope) + StorageFileShare = shared.NewItemType(Azure, Storage, FileShare) + StorageTable = shared.NewItemType(Azure, Storage, Table) + StorageQueue = shared.NewItemType(Azure, Storage, Queue) + StoragePrivateEndpointConnection = shared.NewItemType(Azure, Storage, StorageAccountPrivateEndpointConnection) // SQL item types SQLDatabase = shared.NewItemType(Azure, SQL, Database) @@ -109,6 +116,11 @@ var ( SQLServerTrustGroup = shared.NewItemType(Azure, SQL, ServerTrustGroup) SQLServerOutboundFirewallRule = shared.NewItemType(Azure, SQL, ServerOutboundFirewallRule) SQLServerPrivateLinkResource = shared.NewItemType(Azure, SQL, ServerPrivateLinkResource) + SQLLongTermRetentionBackup = shared.NewItemType(Azure, SQL, LongTermRetentionBackup) + SQLDatabaseSchema = shared.NewItemType(Azure, SQL, DatabaseSchema) + + // Maintenance item types + MaintenanceMaintenanceConfiguration = shared.NewItemType(Azure, Maintenance, MaintenanceConfiguration) // DBforPostgreSQL item types DBforPostgreSQLFlexibleServer = shared.NewItemType(Azure, DBforPostgreSQL, FlexibleServer) @@ -128,10 +140,11 @@ var ( DocumentDBPrivateEndpointConnection = shared.NewItemType(Azure, DocumentDB, PrivateEndpointConnection) // KeyVault item types - KeyVaultVault = shared.NewItemType(Azure, KeyVault, Vault) - KeyVaultSecret = shared.NewItemType(Azure, KeyVault, Secret) - KeyVaultKey = shared.NewItemType(Azure, KeyVault, Key) - KeyVaultManagedHSM = shared.NewItemType(Azure, KeyVault, ManagedHSM) + KeyVaultVault = shared.NewItemType(Azure, KeyVault, Vault) + KeyVaultSecret = shared.NewItemType(Azure, KeyVault, Secret) + KeyVaultKey = shared.NewItemType(Azure, KeyVault, Key) + KeyVaultManagedHSM = shared.NewItemType(Azure, KeyVault, ManagedHSM) + KeyVaultManagedHSMPrivateEndpointConnection = shared.NewItemType(Azure, KeyVault, ManagedHSMPrivateEndpointConnection) // ManagedIdentity item types ManagedIdentityUserAssignedIdentity = shared.NewItemType(Azure, ManagedIdentity, UserAssignedIdentity) diff --git a/sources/azure/shared/mocks/mock_application_gateways_client.go b/sources/azure/shared/mocks/mock_application_gateways_client.go index 200546e3..88a547c5 100644 --- a/sources/azure/shared/mocks/mock_application_gateways_client.go +++ b/sources/azure/shared/mocks/mock_application_gateways_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_batch_accounts_client.go b/sources/azure/shared/mocks/mock_batch_accounts_client.go index 84a847fc..5f3f946c 100644 --- a/sources/azure/shared/mocks/mock_batch_accounts_client.go +++ b/sources/azure/shared/mocks/mock_batch_accounts_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch" + armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_blob_containers_client.go b/sources/azure/shared/mocks/mock_blob_containers_client.go index 6749d968..5bad2597 100644 --- a/sources/azure/shared/mocks/mock_blob_containers_client.go +++ b/sources/azure/shared/mocks/mock_blob_containers_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_file_shares_client.go b/sources/azure/shared/mocks/mock_file_shares_client.go index de63f227..74a56910 100644 --- a/sources/azure/shared/mocks/mock_file_shares_client.go +++ b/sources/azure/shared/mocks/mock_file_shares_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_load_balancers_client.go b/sources/azure/shared/mocks/mock_load_balancers_client.go index 36141ccf..a140c027 100644 --- a/sources/azure/shared/mocks/mock_load_balancers_client.go +++ b/sources/azure/shared/mocks/mock_load_balancers_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_managed_hsms_client.go b/sources/azure/shared/mocks/mock_managed_hsms_client.go index 19e5ad29..52d0655a 100644 --- a/sources/azure/shared/mocks/mock_managed_hsms_client.go +++ b/sources/azure/shared/mocks/mock_managed_hsms_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armkeyvault "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + armkeyvault "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_network_interfaces_client.go b/sources/azure/shared/mocks/mock_network_interfaces_client.go index aceea54d..6547f1c9 100644 --- a/sources/azure/shared/mocks/mock_network_interfaces_client.go +++ b/sources/azure/shared/mocks/mock_network_interfaces_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_network_security_groups_client.go b/sources/azure/shared/mocks/mock_network_security_groups_client.go index ccee9e9b..734d3a13 100644 --- a/sources/azure/shared/mocks/mock_network_security_groups_client.go +++ b/sources/azure/shared/mocks/mock_network_security_groups_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_proximity_placement_groups_client.go b/sources/azure/shared/mocks/mock_proximity_placement_groups_client.go new file mode 100644 index 00000000..daac2be9 --- /dev/null +++ b/sources/azure/shared/mocks/mock_proximity_placement_groups_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: proximity-placement-groups-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_proximity_placement_groups_client.go -package=mocks -source=proximity-placement-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" +) + +// MockProximityPlacementGroupsClient is a mock of ProximityPlacementGroupsClient interface. +type MockProximityPlacementGroupsClient struct { + ctrl *gomock.Controller + recorder *MockProximityPlacementGroupsClientMockRecorder + isgomock struct{} +} + +// MockProximityPlacementGroupsClientMockRecorder is the mock recorder for MockProximityPlacementGroupsClient. +type MockProximityPlacementGroupsClientMockRecorder struct { + mock *MockProximityPlacementGroupsClient +} + +// NewMockProximityPlacementGroupsClient creates a new mock instance. +func NewMockProximityPlacementGroupsClient(ctrl *gomock.Controller) *MockProximityPlacementGroupsClient { + mock := &MockProximityPlacementGroupsClient{ctrl: ctrl} + mock.recorder = &MockProximityPlacementGroupsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProximityPlacementGroupsClient) EXPECT() *MockProximityPlacementGroupsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockProximityPlacementGroupsClient) Get(ctx context.Context, resourceGroupName, proximityPlacementGroupName string, options *armcompute.ProximityPlacementGroupsClientGetOptions) (armcompute.ProximityPlacementGroupsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, proximityPlacementGroupName, options) + ret0, _ := ret[0].(armcompute.ProximityPlacementGroupsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockProximityPlacementGroupsClientMockRecorder) Get(ctx, resourceGroupName, proximityPlacementGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockProximityPlacementGroupsClient)(nil).Get), ctx, resourceGroupName, proximityPlacementGroupName, options) +} + +// ListByResourceGroup mocks base method. +func (m *MockProximityPlacementGroupsClient) ListByResourceGroup(ctx context.Context, resourceGroupName string, options *armcompute.ProximityPlacementGroupsClientListByResourceGroupOptions) clients.ProximityPlacementGroupsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByResourceGroup", ctx, resourceGroupName, options) + ret0, _ := ret[0].(clients.ProximityPlacementGroupsPager) + return ret0 +} + +// ListByResourceGroup indicates an expected call of ListByResourceGroup. +func (mr *MockProximityPlacementGroupsClientMockRecorder) ListByResourceGroup(ctx, resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByResourceGroup", reflect.TypeOf((*MockProximityPlacementGroupsClient)(nil).ListByResourceGroup), ctx, resourceGroupName, options) +} diff --git a/sources/azure/shared/mocks/mock_public_ip_addresses_client.go b/sources/azure/shared/mocks/mock_public_ip_addresses_client.go index 44929687..29297475 100644 --- a/sources/azure/shared/mocks/mock_public_ip_addresses_client.go +++ b/sources/azure/shared/mocks/mock_public_ip_addresses_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_queues_client.go b/sources/azure/shared/mocks/mock_queues_client.go index 63831c4f..4fe086a1 100644 --- a/sources/azure/shared/mocks/mock_queues_client.go +++ b/sources/azure/shared/mocks/mock_queues_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_route_tables_client.go b/sources/azure/shared/mocks/mock_route_tables_client.go index 753c4b8e..a37bf0c0 100644 --- a/sources/azure/shared/mocks/mock_route_tables_client.go +++ b/sources/azure/shared/mocks/mock_route_tables_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_secrets_client.go b/sources/azure/shared/mocks/mock_secrets_client.go index 5c8d8fbc..395a081e 100644 --- a/sources/azure/shared/mocks/mock_secrets_client.go +++ b/sources/azure/shared/mocks/mock_secrets_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armkeyvault "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + armkeyvault "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_sql_databases_client.go b/sources/azure/shared/mocks/mock_sql_databases_client.go index 1997abb8..e4bbfeff 100644 --- a/sources/azure/shared/mocks/mock_sql_databases_client.go +++ b/sources/azure/shared/mocks/mock_sql_databases_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armsql "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + armsql "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_sql_servers_client.go b/sources/azure/shared/mocks/mock_sql_servers_client.go index 760f6a5a..d94a0df1 100644 --- a/sources/azure/shared/mocks/mock_sql_servers_client.go +++ b/sources/azure/shared/mocks/mock_sql_servers_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armsql "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + armsql "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_storage_accounts_client.go b/sources/azure/shared/mocks/mock_storage_accounts_client.go index cc8ce140..125cc808 100644 --- a/sources/azure/shared/mocks/mock_storage_accounts_client.go +++ b/sources/azure/shared/mocks/mock_storage_accounts_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_storage_accounts_pager.go b/sources/azure/shared/mocks/mock_storage_accounts_pager.go index c22a3d5b..1d23c542 100644 --- a/sources/azure/shared/mocks/mock_storage_accounts_pager.go +++ b/sources/azure/shared/mocks/mock_storage_accounts_pager.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_tables_client.go b/sources/azure/shared/mocks/mock_tables_client.go index 23c902f4..2b510e12 100644 --- a/sources/azure/shared/mocks/mock_tables_client.go +++ b/sources/azure/shared/mocks/mock_tables_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v2" + armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_vaults_client.go b/sources/azure/shared/mocks/mock_vaults_client.go index 088c1d2e..4032012d 100644 --- a/sources/azure/shared/mocks/mock_vaults_client.go +++ b/sources/azure/shared/mocks/mock_vaults_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armkeyvault "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + armkeyvault "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_virtual_networks_client.go b/sources/azure/shared/mocks/mock_virtual_networks_client.go index 9dd8943b..d979f6f1 100644 --- a/sources/azure/shared/mocks/mock_virtual_networks_client.go +++ b/sources/azure/shared/mocks/mock_virtual_networks_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index 70792ae3..572659ac 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -43,6 +43,9 @@ const ( // Authorization Authorization shared.API = "authorization" // Microsoft.Authorization + + // Maintenance + Maintenance shared.API = "maintenance" // Microsoft.Maintenance ) // Resources @@ -112,13 +115,20 @@ const ( Zone shared.Resource = "zone" DNSRecordSet shared.Resource = "dns-record-set" DNSVirtualNetworkLink shared.Resource = "dns-virtual-network-link" + FlowLog shared.Resource = "flow-log" + PrivateLinkService shared.Resource = "private-link-service" + DscpConfiguration shared.Resource = "dscp-configuration" + VirtualNetworkTap shared.Resource = "virtual-network-tap" + NetworkInterfaceTapConfiguration shared.Resource = "network-interface-tap-configuration" // Storage resources - Account shared.Resource = "account" - BlobContainer shared.Resource = "blob-container" - FileShare shared.Resource = "file-share" - Table shared.Resource = "table" - Queue shared.Resource = "queue" + Account shared.Resource = "account" + BlobContainer shared.Resource = "blob-container" + EncryptionScope shared.Resource = "encryption-scope" + FileShare shared.Resource = "file-share" + Table shared.Resource = "table" + Queue shared.Resource = "queue" + StorageAccountPrivateEndpointConnection shared.Resource = "storage-account-private-endpoint-connection" // SQL resources Database shared.Resource = "database" @@ -151,6 +161,11 @@ const ( ServerTrustGroup shared.Resource = "server-trust-group" ServerOutboundFirewallRule shared.Resource = "server-outbound-firewall-rule" ServerPrivateLinkResource shared.Resource = "server-private-link-resource" + LongTermRetentionBackup shared.Resource = "long-term-retention-backup" + DatabaseSchema shared.Resource = "database-schema" + + // Maintenance resources + MaintenanceConfiguration shared.Resource = "maintenance-configuration" // DBforPostgreSQL resources FlexibleServer shared.Resource = "flexible-server" @@ -169,10 +184,11 @@ const ( PrivateEndpointConnection shared.Resource = "private-endpoint-connection" // KeyVault resources - Vault shared.Resource = "vault" - Secret shared.Resource = "secret" - Key shared.Resource = "key" - ManagedHSM shared.Resource = "managed-hsm" + Vault shared.Resource = "vault" + Secret shared.Resource = "secret" + Key shared.Resource = "key" + ManagedHSM shared.Resource = "managed-hsm" + ManagedHSMPrivateEndpointConnection shared.Resource = "managed-hsm-private-endpoint-connection" // ManagedIdentity resources UserAssignedIdentity shared.Resource = "user-assigned-identity" diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 3df59672..adb4746e 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -27,7 +27,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "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-virtual-machine-extension": {"virtualMachines", "extensions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/extensions/{extensionName}", } if keys, ok := pathKeysMap[resourceType]; ok { @@ -176,6 +176,22 @@ func ExtractSQLRestorableDroppedDatabaseInfoFromResourceID(resourceID string) (s return "", "" } +// ExtractSQLLongTermRetentionBackupInfoFromResourceID extracts parameters from a long term retention backup resource ID. +// Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/locations/{location}/longTermRetentionServers/{server}/longTermRetentionDatabases/{db}/longTermRetentionBackups/{backupName} +// Returns locationName, serverName, databaseName, backupName if valid, otherwise empty strings. +func ExtractSQLLongTermRetentionBackupInfoFromResourceID(resourceID string) (locationName, serverName, databaseName, backupName string) { + if resourceID == "" { + return "", "", "", "" + } + + params := ExtractPathParamsFromResourceID(resourceID, []string{"locations", "longTermRetentionServers", "longTermRetentionDatabases", "longTermRetentionBackups"}) + if len(params) >= 4 { + return params[0], params[1], params[2], params[3] + } + + return "", "", "", "" +} + // ExtractSQLElasticPoolInfoFromResourceID extracts SQL server name and elastic pool name from a SQL elastic pool resource ID. // Azure SQL elastic pool IDs follow the format: // /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Sql/servers/{serverName}/elasticPools/{elasticPoolName} diff --git a/sources/azure/shared/utils_test.go b/sources/azure/shared/utils_test.go index 9f00d2bd..e90dc62e 100644 --- a/sources/azure/shared/utils_test.go +++ b/sources/azure/shared/utils_test.go @@ -355,29 +355,29 @@ func TestExtractSQLServerNameFromDatabaseID(t *testing.T) { func TestExtractSQLElasticPoolNameFromID(t *testing.T) { tests := []struct { - name string + name string elasticPoolID string - expected string + expected string }{ { - name: "valid SQL elastic pool resource ID", + name: "valid SQL elastic pool resource ID", elasticPoolID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/elasticPools/test-pool", - expected: "test-pool", + expected: "test-pool", }, { - name: "empty elastic pool ID", + name: "empty elastic pool ID", elasticPoolID: "", - expected: "", + expected: "", }, { - name: "invalid resource ID format", + name: "invalid resource ID format", elasticPoolID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/test-account", - expected: "", + expected: "", }, { - name: "resource ID without elasticPools segment", + name: "resource ID without elasticPools segment", elasticPoolID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server", - expected: "", + expected: "", }, } @@ -393,34 +393,34 @@ func TestExtractSQLElasticPoolNameFromID(t *testing.T) { func TestExtractSQLDatabaseInfoFromResourceID(t *testing.T) { tests := []struct { - name string - resourceID string + name string + resourceID string expectedServer string - expectedDB string + expectedDB string }{ { name: "valid SQL database resource ID", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/test-db", expectedServer: "test-server", - expectedDB: "test-db", + expectedDB: "test-db", }, { name: "empty resource ID", resourceID: "", expectedServer: "", - expectedDB: "", + expectedDB: "", }, { name: "invalid resource ID format", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/test-account", expectedServer: "", - expectedDB: "", + expectedDB: "", }, { name: "resource ID missing databases segment", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server", expectedServer: "", - expectedDB: "", + expectedDB: "", }, } @@ -439,34 +439,34 @@ func TestExtractSQLDatabaseInfoFromResourceID(t *testing.T) { func TestExtractSQLRecoverableDatabaseInfoFromResourceID(t *testing.T) { tests := []struct { - name string - resourceID string + name string + resourceID string expectedServer string - expectedDB string + expectedDB string }{ { name: "valid recoverable database resource ID", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/recoverableDatabases/test-db", expectedServer: "test-server", - expectedDB: "test-db", + expectedDB: "test-db", }, { name: "empty resource ID", resourceID: "", expectedServer: "", - expectedDB: "", + expectedDB: "", }, { name: "invalid resource ID format", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/test-account", expectedServer: "", - expectedDB: "", + expectedDB: "", }, { name: "resource ID missing recoverableDatabases segment", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server", expectedServer: "", - expectedDB: "", + expectedDB: "", }, } @@ -485,34 +485,34 @@ func TestExtractSQLRecoverableDatabaseInfoFromResourceID(t *testing.T) { func TestExtractSQLRestorableDroppedDatabaseInfoFromResourceID(t *testing.T) { tests := []struct { - name string - resourceID string + name string + resourceID string expectedServer string - expectedDB string + expectedDB string }{ { name: "valid restorable dropped database resource ID", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/restorableDroppedDatabases/test-db", expectedServer: "test-server", - expectedDB: "test-db", + expectedDB: "test-db", }, { name: "empty resource ID", resourceID: "", expectedServer: "", - expectedDB: "", + expectedDB: "", }, { name: "invalid resource ID format", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/test-account", expectedServer: "", - expectedDB: "", + expectedDB: "", }, { name: "resource ID missing restorableDroppedDatabases segment", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server", expectedServer: "", - expectedDB: "", + expectedDB: "", }, } @@ -531,34 +531,34 @@ func TestExtractSQLRestorableDroppedDatabaseInfoFromResourceID(t *testing.T) { func TestExtractSQLElasticPoolInfoFromResourceID(t *testing.T) { tests := []struct { - name string - resourceID string + name string + resourceID string expectedServer string - expectedPool string + expectedPool string }{ { name: "valid SQL elastic pool resource ID", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/elasticPools/test-pool", expectedServer: "test-server", - expectedPool: "test-pool", + expectedPool: "test-pool", }, { name: "empty resource ID", resourceID: "", expectedServer: "", - expectedPool: "", + expectedPool: "", }, { name: "invalid resource ID format", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/test-account", expectedServer: "", - expectedPool: "", + expectedPool: "", }, { name: "resource ID missing elasticPools segment", resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server", expectedServer: "", - expectedPool: "", + expectedPool: "", }, } @@ -575,6 +575,60 @@ func TestExtractSQLElasticPoolInfoFromResourceID(t *testing.T) { } } +func TestExtractSQLLongTermRetentionBackupInfoFromResourceID(t *testing.T) { + tests := []struct { + name string + resourceID string + expectedLocation string + expectedServer string + expectedDatabase string + expectedBackupName string + }{ + { + name: "valid SQL long term retention backup resource ID", + resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/locations/eastus/longTermRetentionServers/test-server/longTermRetentionDatabases/test-db/longTermRetentionBackups/1234567890;1234567890", + expectedLocation: "eastus", + expectedServer: "test-server", + expectedDatabase: "test-db", + expectedBackupName: "1234567890;1234567890", + }, + { + name: "empty resource ID", + resourceID: "", + expectedLocation: "", + expectedServer: "", + expectedDatabase: "", + expectedBackupName: "", + }, + { + name: "invalid resource ID format", + resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/test-db", + expectedLocation: "", + expectedServer: "", + expectedDatabase: "", + expectedBackupName: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actualLocation, actualServer, actualDatabase, actualBackupName := azureshared.ExtractSQLLongTermRetentionBackupInfoFromResourceID(tc.resourceID) + if actualLocation != tc.expectedLocation { + t.Errorf("ExtractSQLLongTermRetentionBackupInfoFromResourceID(%q) location = %q; want %q", tc.resourceID, actualLocation, tc.expectedLocation) + } + if actualServer != tc.expectedServer { + t.Errorf("ExtractSQLLongTermRetentionBackupInfoFromResourceID(%q) server = %q; want %q", tc.resourceID, actualServer, tc.expectedServer) + } + if actualDatabase != tc.expectedDatabase { + t.Errorf("ExtractSQLLongTermRetentionBackupInfoFromResourceID(%q) database = %q; want %q", tc.resourceID, actualDatabase, tc.expectedDatabase) + } + if actualBackupName != tc.expectedBackupName { + t.Errorf("ExtractSQLLongTermRetentionBackupInfoFromResourceID(%q) backupName = %q; want %q", tc.resourceID, actualBackupName, tc.expectedBackupName) + } + }) + } +} + func TestDetermineSourceResourceType(t *testing.T) { tests := []struct { name string @@ -583,8 +637,8 @@ func TestDetermineSourceResourceType(t *testing.T) { expectedParams map[string]string }{ { - name: "SQL database resource ID", - resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/test-db", + name: "SQL database resource ID", + resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/test-db", expectedType: azureshared.SourceResourceTypeSQLDatabase, expectedParams: map[string]string{ "serverName": "test-server", @@ -592,8 +646,8 @@ func TestDetermineSourceResourceType(t *testing.T) { }, }, { - name: "SQL elastic pool resource ID", - resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/elasticPools/test-pool", + name: "SQL elastic pool resource ID", + resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/elasticPools/test-pool", expectedType: azureshared.SourceResourceTypeSQLElasticPool, expectedParams: map[string]string{ "serverName": "test-server", @@ -601,21 +655,21 @@ func TestDetermineSourceResourceType(t *testing.T) { }, }, { - name: "empty resource ID", - resourceID: "", - expectedType: azureshared.SourceResourceTypeUnknown, + name: "empty resource ID", + resourceID: "", + expectedType: azureshared.SourceResourceTypeUnknown, expectedParams: nil, }, { - name: "unknown resource type", - resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/test-account", - expectedType: azureshared.SourceResourceTypeUnknown, + name: "unknown resource type", + resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/test-account", + expectedType: azureshared.SourceResourceTypeUnknown, expectedParams: nil, }, { - name: "Synapse SQL pool (not yet supported)", - resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Synapse/workspaces/test-workspace/sqlPools/test-pool", - expectedType: azureshared.SourceResourceTypeUnknown, + name: "Synapse SQL pool (not yet supported)", + resourceID: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Synapse/workspaces/test-workspace/sqlPools/test-pool", + expectedType: azureshared.SourceResourceTypeUnknown, expectedParams: nil, }, } diff --git a/sources/gcp/cmd/root.go b/sources/gcp/cmd/root.go index 4d8bf3ae..10ddbfa2 100644 --- a/sources/gcp/cmd/root.go +++ b/sources/gcp/cmd/root.go @@ -3,12 +3,10 @@ package cmd import ( "context" "fmt" - "net/http" "os" "os/signal" "strings" "syscall" - "time" "github.com/getsentry/sentry-go" "github.com/overmindtech/cli/logging" @@ -53,35 +51,7 @@ var rootCmd = &cobra.Command{ e.StartSendingHeartbeats(ctx) - // Start HTTP server for health checks - // Liveness: Check only engine initialization (NATS, heartbeats) - http.HandleFunc("/healthz/alive", e.LivenessProbeHandlerFunc()) - // Readiness: Check if adapters are healthy and ready to handle requests - http.HandleFunc("/healthz/ready", e.ReadinessProbeHandlerFunc()) - // Backward compatibility - maps to liveness check (matches old behavior) - http.HandleFunc("/healthz", e.LivenessProbeHandlerFunc()) - - log.WithFields(log.Fields{ - "ovm.source.type": "gcp", - "ovm.source.port": healthCheckPort, - }).Debug("Starting healthcheck server with endpoints: /healthz/alive, /healthz/ready, /healthz") - - go func() { - defer sentry.Recover() - - server := &http.Server{ - Addr: fmt.Sprintf(":%v", healthCheckPort), - Handler: nil, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - err := server.ListenAndServe() - - log.WithError(err).WithFields(log.Fields{ - "ovm.source.type": "gcp", - "ovm.source.port": healthCheckPort, - }).Error("Could not start HTTP server for health checks") - }() + e.ServeHealthProbes(healthCheckPort) err = e.Start(ctx) if err != nil { diff --git a/sources/gcp/integration-tests/README.md b/sources/gcp/integration-tests/README.md index fb5bb26a..3a20b540 100644 --- a/sources/gcp/integration-tests/README.md +++ b/sources/gcp/integration-tests/README.md @@ -20,6 +20,8 @@ For example, `TestComputeInstanceIntegration` tests the Compute API's Instance r cloudresourcemanager.googleapis.com \ iam.googleapis.com \ iamcredentials.googleapis.com \ + cloudkms.googleapis.com \ + cloudasset.googleapis.com \ --project=integration-tests-484908 ``` diff --git a/sources/gcp/integration-tests/kms_vs_asset_inventory_test.go b/sources/gcp/integration-tests/kms_vs_asset_inventory_test.go new file mode 100644 index 00000000..0d23eb9e --- /dev/null +++ b/sources/gcp/integration-tests/kms_vs_asset_inventory_test.go @@ -0,0 +1,436 @@ +package integrationtests + +// GCP Cloud KMS Limitations +// +// This test compares the Cloud KMS direct API with the Cloud Asset Inventory API. +// Understanding the following GCP limitations is essential for working with KMS resources: +// +// 1. CryptoKey Deletion: +// - CryptoKeys CANNOT be immediately deleted from GCP +// - Must destroy all CryptoKeyVersions first (schedules for deletion after 24h by default) +// - Even after version destruction, the CryptoKey resource remains (in DESTROYED state) +// - The key name cannot be reused after destruction +// - See: https://cloud.google.com/kms/docs/destroy-restore +// +// 2. KeyRing Deletion: +// - KeyRings CANNOT be deleted at all in GCP +// - Once created, they persist forever in the project +// - This is by design for audit/compliance purposes +// - See: https://cloud.google.com/kms/docs/resource-hierarchy +// +// 3. Resource Naming: +// - KeyRing and CryptoKey names must be unique within their parent +// - Names cannot be reused even after destruction +// - This test uses a shared KeyRing to avoid proliferation +// +// 4. Asset Inventory Indexing: +// - Cloud Asset Inventory indexes resources asynchronously +// - New resources may take 1-5 minutes to appear in queries +// - The test includes retry logic to handle this delay +// +// API Rate Limits (for reference): +// +// Cloud KMS API: +// - Read requests: 300 queries per minute (QPM) +// - Enforced per-second (QPS), not per-minute +// - Exceeding limit returns RESOURCE_EXHAUSTED error +// - See: https://cloud.google.com/kms/quotas +// +// Cloud Asset Inventory API: +// - ListAssets: 100 QPM per project, 800 QPM per organization +// - SearchAllResources: 400 QPM per project +// - See: https://cloud.google.com/asset-inventory/docs/quota + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "testing" + "time" + + kms "cloud.google.com/go/kms/apiv1" + "cloud.google.com/go/kms/apiv1/kmspb" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" +) + +const ( + // Shared KeyRing name - reused across test runs since KeyRings cannot be deleted + testKeyRingName = "integration-test-keyring" + // Location for KMS resources + testKMSLocation = "global" + // CryptoKey name prefix - timestamp will be appended for uniqueness + testCryptoKeyPrefix = "api-comparison-test-key" +) + +// TestKMSvsAssetInventoryComparison compares the Cloud KMS direct API with the +// Cloud Asset Inventory API for retrieving CryptoKey information. +// +// This test demonstrates the differences in: +// - Calling conventions (URL structure, query parameters) +// - Response structure (direct resource vs wrapped asset) +// - Available metadata (ancestors, update times, etc.) +// - Rate limits and quotas +func TestKMSvsAssetInventoryComparison(t *testing.T) { + projectID := os.Getenv("GCP_PROJECT_ID") + if projectID == "" { + t.Skip("GCP_PROJECT_ID environment variable not set") + } + + ctx := context.Background() + + // Create KMS client for resource management + kmsClient, err := kms.NewKeyManagementClient(ctx) + if err != nil { + t.Fatalf("Failed to create KMS client: %v", err) + } + defer kmsClient.Close() + + // Create HTTP client for direct API calls + httpClient, err := gcpshared.GCPHTTPClientWithOtel(ctx, "") + if err != nil { + t.Fatalf("Failed to create HTTP client: %v", err) + } + + // Generate unique CryptoKey name for this test run + cryptoKeyName := fmt.Sprintf("%s-%d", testCryptoKeyPrefix, time.Now().Unix()) + + // Full resource names + keyRingParent := fmt.Sprintf("projects/%s/locations/%s", projectID, testKMSLocation) + keyRingFullName := fmt.Sprintf("%s/keyRings/%s", keyRingParent, testKeyRingName) + cryptoKeyFullName := fmt.Sprintf("%s/cryptoKeys/%s", keyRingFullName, cryptoKeyName) + + t.Run("Setup", func(t *testing.T) { + // Create KeyRing (idempotent - will succeed if already exists) + err := createKeyRing(ctx, kmsClient, keyRingParent, testKeyRingName) + if err != nil { + t.Fatalf("Failed to create KeyRing: %v", err) + } + log.Printf("KeyRing ready: %s", keyRingFullName) + + // Create CryptoKey for this test + err = createCryptoKey(ctx, kmsClient, keyRingFullName, cryptoKeyName) + if err != nil { + t.Fatalf("Failed to create CryptoKey: %v", err) + } + log.Printf("CryptoKey created: %s", cryptoKeyFullName) + }) + + t.Run("CompareAPIs", func(t *testing.T) { + t.Log("=== GCP API Comparison: Cloud KMS vs Cloud Asset Inventory ===") + t.Log("") + + // --- Cloud KMS Direct API --- + t.Log("--- Cloud KMS Direct API ---") + kmsURL := fmt.Sprintf("https://cloudkms.googleapis.com/v1/%s", cryptoKeyFullName) + t.Logf("URL: %s", kmsURL) + t.Logf("Method: GET") + t.Logf("Required Permission: cloudkms.cryptoKeys.get") + t.Logf("Rate Limit: 300 QPM (enforced per-second)") + t.Log("") + + kmsStart := time.Now() + kmsResponse, err := callKMSDirectAPI(ctx, httpClient, cryptoKeyFullName) + kmsLatency := time.Since(kmsStart) + if err != nil { + t.Fatalf("Failed to call KMS API: %v", err) + } + t.Logf("Latency: %v", kmsLatency) + t.Log("") + + // Pretty print KMS response + kmsJSON, _ := json.MarshalIndent(kmsResponse, "", " ") + t.Logf("Response Structure (Cloud KMS):\n%s", string(kmsJSON)) + t.Log("") + + // --- Cloud Asset Inventory API --- + t.Log("--- Cloud Asset Inventory API ---") + assetURL := fmt.Sprintf( + "https://cloudasset.googleapis.com/v1/projects/%s/assets?assetTypes=cloudkms.googleapis.com/CryptoKey&contentType=RESOURCE", + projectID, + ) + t.Logf("URL: %s", assetURL) + t.Logf("Method: GET") + t.Logf("Required Permission: cloudasset.assets.listResource") + t.Logf("Rate Limit: 100 QPM per project (ListAssets)") + t.Log("") + + // Asset Inventory may have indexing delay - retry with backoff + var assetResponse map[string]interface{} + var assetLatency time.Duration + var foundAsset bool + + t.Log("Note: Cloud Asset Inventory indexes resources asynchronously.") + t.Log("Retrying with backoff if the newly created key is not yet indexed...") + t.Log("") + + for attempt := 1; attempt <= 10; attempt++ { + assetStart := time.Now() + assetResponse, err = callAssetInventoryAPI(ctx, httpClient, projectID, cryptoKeyFullName) + assetLatency = time.Since(assetStart) + + if err != nil { + t.Logf("Attempt %d: Error calling Asset Inventory API: %v", attempt, err) + } else if assetResponse != nil { + foundAsset = true + t.Logf("Attempt %d: Found asset after %v", attempt, assetLatency) + break + } else { + t.Logf("Attempt %d: Asset not yet indexed, waiting...", attempt) + } + + // Exponential backoff: 5s, 10s, 20s, 40s... up to 60s + waitTime := time.Duration(5*(1<<(attempt-1))) * time.Second + if waitTime > 60*time.Second { + waitTime = 60 * time.Second + } + time.Sleep(waitTime) + } + + if !foundAsset { + t.Log("WARNING: Asset not found in Cloud Asset Inventory after retries.") + t.Log("This may indicate the indexing delay exceeds our retry window.") + t.Log("The test will continue with partial comparison.") + } else { + // Pretty print Asset Inventory response + assetJSON, _ := json.MarshalIndent(assetResponse, "", " ") + t.Logf("Response Structure (Cloud Asset Inventory):\n%s", string(assetJSON)) + } + t.Log("") + + // --- Comparison Summary --- + t.Log("=== Comparison Summary ===") + t.Log("") + t.Log("| Aspect | Cloud KMS API | Cloud Asset Inventory API |") + t.Log("|-------------------------|----------------------------|---------------------------------|") + t.Log("| Endpoint | cloudkms.googleapis.com | cloudasset.googleapis.com |") + t.Log("| Response Type | Direct resource | Wrapped in Asset object |") + t.Log("| Resource Data Location | Root of response | resource.data field |") + t.Log("| Rate Limit | 300 QPM | 100 QPM (ListAssets) |") + t.Log("| Ancestry Info | Not included | Included (ancestors field) |") + t.Log("| IAM Policy | Separate API call | Optional (contentType param) |") + t.Log("| Update Timestamp | createTime only | updateTime + createTime |") + t.Logf("| Observed Latency | %v | %v |", kmsLatency.Round(time.Millisecond), assetLatency.Round(time.Millisecond)) + t.Log("") + + t.Log("Key Differences:") + t.Log("1. Cloud KMS returns the CryptoKey resource directly") + t.Log("2. Cloud Asset Inventory wraps the resource with metadata (ancestors, assetType, updateTime)") + t.Log("3. Asset Inventory can batch multiple asset types in a single request") + t.Log("4. Asset Inventory provides resource hierarchy information (ancestors)") + t.Log("5. Cloud KMS API has higher rate limits for targeted resource access") + t.Log("6. Asset Inventory has indexing delay (resources not immediately available)") + }) + + t.Run("Teardown", func(t *testing.T) { + // Note: We cannot delete CryptoKeys or KeyRings in GCP. + // The best we can do is destroy the CryptoKeyVersion to make the key unusable. + // + // From GCP documentation: + // "You cannot delete a CryptoKey or KeyRing resource. These resources are retained + // indefinitely for audit and compliance purposes." + // + // To minimize resource accumulation, we: + // 1. Destroy the primary CryptoKeyVersion (schedules it for deletion after 24h) + // 2. Leave the CryptoKey in DESTROYED state + // 3. Reuse the same KeyRing for all test runs + + err := destroyCryptoKeyVersion(ctx, kmsClient, cryptoKeyFullName) + if err != nil { + // Log but don't fail - the key will remain but be unusable + log.Printf("Warning: Failed to destroy CryptoKeyVersion: %v", err) + log.Printf("The CryptoKey %s will remain active but can be manually destroyed later", cryptoKeyFullName) + } else { + log.Printf("CryptoKeyVersion scheduled for destruction: %s", cryptoKeyFullName) + log.Printf("Note: The CryptoKey resource itself cannot be deleted (GCP limitation)") + } + }) +} + +// createKeyRing creates a KeyRing if it doesn't already exist. +// KeyRings cannot be deleted, so this is idempotent. +func createKeyRing(ctx context.Context, client *kms.KeyManagementClient, parent, keyRingID string) error { + req := &kmspb.CreateKeyRingRequest{ + Parent: parent, + KeyRingId: keyRingID, + KeyRing: &kmspb.KeyRing{}, + } + + _, err := client.CreateKeyRing(ctx, req) + if err != nil { + // Check for gRPC AlreadyExists error - KeyRing already exists is fine + if st, ok := status.FromError(err); ok && st.Code() == codes.AlreadyExists { + log.Printf("KeyRing already exists (expected): %s/%s", parent, keyRingID) + return nil + } + return fmt.Errorf("failed to create KeyRing: %w", err) + } + + return nil +} + +// createCryptoKey creates a new CryptoKey for encryption/decryption. +func createCryptoKey(ctx context.Context, client *kms.KeyManagementClient, keyRingName, cryptoKeyID string) error { + req := &kmspb.CreateCryptoKeyRequest{ + Parent: keyRingName, + CryptoKeyId: cryptoKeyID, + CryptoKey: &kmspb.CryptoKey{ + Purpose: kmspb.CryptoKey_ENCRYPT_DECRYPT, + Labels: map[string]string{ + "test": "integration", + "purpose": "api-comparison", + }, + }, + } + + _, err := client.CreateCryptoKey(ctx, req) + if err != nil { + return fmt.Errorf("failed to create CryptoKey: %w", err) + } + + return nil +} + +// destroyCryptoKeyVersion destroys the primary version of a CryptoKey. +// This is the closest we can get to "deleting" a key in GCP. +// The version is scheduled for destruction after 24 hours by default. +func destroyCryptoKeyVersion(ctx context.Context, client *kms.KeyManagementClient, cryptoKeyName string) error { + // First, get the CryptoKey to find its primary version + getReq := &kmspb.GetCryptoKeyRequest{ + Name: cryptoKeyName, + } + + cryptoKey, err := client.GetCryptoKey(ctx, getReq) + if err != nil { + return fmt.Errorf("failed to get CryptoKey: %w", err) + } + + if cryptoKey.GetPrimary() == nil { + log.Printf("CryptoKey has no primary version (may already be destroyed)") + return nil + } + + // Destroy the primary version + destroyReq := &kmspb.DestroyCryptoKeyVersionRequest{ + Name: cryptoKey.GetPrimary().GetName(), + } + + _, err = client.DestroyCryptoKeyVersion(ctx, destroyReq) + if err != nil { + return fmt.Errorf("failed to destroy CryptoKeyVersion: %w", err) + } + + return nil +} + +// callKMSDirectAPI calls the Cloud KMS REST API directly to get a CryptoKey. +func callKMSDirectAPI(ctx context.Context, httpClient *http.Client, cryptoKeyName string) (map[string]interface{}, error) { + apiURL := fmt.Sprintf("https://cloudkms.googleapis.com/v1/%s", cryptoKeyName) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("KMS API returned status %d: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return result, nil +} + +// callAssetInventoryAPI calls the Cloud Asset Inventory API to find a specific CryptoKey. +// Returns the asset if found, nil if not found (may indicate indexing delay). +func callAssetInventoryAPI(ctx context.Context, httpClient *http.Client, projectID, cryptoKeyName string) (map[string]interface{}, error) { + // Build the Asset Inventory ListAssets URL + baseURL := fmt.Sprintf("https://cloudasset.googleapis.com/v1/projects/%s/assets", projectID) + + params := url.Values{} + params.Set("assetTypes", "cloudkms.googleapis.com/CryptoKey") + params.Set("contentType", "RESOURCE") + + apiURL := fmt.Sprintf("%s?%s", baseURL, params.Encode()) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Cloud Asset Inventory API requires a quota project header when using user credentials + // This tells GCP which project to bill for the API usage + req.Header.Set("X-Goog-User-Project", projectID) + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("Asset Inventory API returned status %d: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + // Find the specific CryptoKey in the assets list + assets, ok := result["assets"].([]interface{}) + if !ok || len(assets) == 0 { + return nil, nil // No assets found - may indicate indexing delay + } + + // The Asset Inventory uses full resource names with // prefix + // e.g., //cloudkms.googleapis.com/projects/PROJECT/locations/global/keyRings/RING/cryptoKeys/KEY + expectedAssetName := fmt.Sprintf("//cloudkms.googleapis.com/%s", cryptoKeyName) + + for _, asset := range assets { + assetMap, ok := asset.(map[string]interface{}) + if !ok { + continue + } + + name, ok := assetMap["name"].(string) + if !ok { + continue + } + + if strings.HasSuffix(name, cryptoKeyName) || name == expectedAssetName { + return assetMap, nil + } + } + + return nil, nil // Specific key not found in results +} diff --git a/sources/gcp/manual/adapters.go b/sources/gcp/manual/adapters.go index e41b1fad..c43e3f21 100644 --- a/sources/gcp/manual/adapters.go +++ b/sources/gcp/manual/adapters.go @@ -7,7 +7,6 @@ import ( "cloud.google.com/go/bigquery" compute "cloud.google.com/go/compute/apiv1" iamAdmin "cloud.google.com/go/iam/admin/apiv1" - kms "cloud.google.com/go/kms/apiv1" logging "cloud.google.com/go/logging/apiv2" "golang.org/x/oauth2" "google.golang.org/api/option" @@ -35,15 +34,14 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati computeSnapshotCli *compute.SnapshotsClient computeInstantSnapshotCli *compute.InstantSnapshotsClient computeMachineImageCli *compute.MachineImagesClient - backendServiceCli *compute.BackendServicesClient - instanceGroupCli *compute.InstanceGroupsClient - instanceGroupManagerCli *compute.InstanceGroupManagersClient - diskCli *compute.DisksClient + backendServiceCli *compute.BackendServicesClient + instanceGroupCli *compute.InstanceGroupsClient + instanceGroupManagerCli *compute.InstanceGroupManagersClient + regionInstanceGroupManagerCli *compute.RegionInstanceGroupManagersClient + diskCli *compute.DisksClient iamServiceAccountKeyCli *iamAdmin.IamClient iamServiceAccountCli *iamAdmin.IamClient - kmsKeyRingCli *kms.KeyManagementClient - kmsCryptoKeyCli *kms.KeyManagementClient - kmsCryptoKeyVersionCli *kms.KeyManagementClient + kmsLoader *shared.CloudKMSAssetLoader bigQueryDatasetCli *bigquery.Client loggingConfigCli *logging.ConfigClient nodeGroupCli *compute.NodeGroupsClient @@ -128,6 +126,11 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati return nil, fmt.Errorf("failed to create compute instance group managers client: %w", err) } + regionInstanceGroupManagerCli, err = compute.NewRegionInstanceGroupManagersRESTClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("failed to create compute region instance group managers client: %w", err) + } + diskCli, err = compute.NewDisksRESTClient(ctx, opts...) if err != nil { return nil, fmt.Errorf("failed to create compute disks client: %w", err) @@ -144,22 +147,6 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati return nil, fmt.Errorf("failed to create IAM service account client: %w", err) } - // KMS - kmsKeyRingCli, err = kms.NewKeyManagementClient(ctx, opts...) - if err != nil { - return nil, fmt.Errorf("failed to create KMS key ring client: %w", err) - } - - kmsCryptoKeyCli, err = kms.NewKeyManagementClient(ctx, opts...) - if err != nil { - return nil, fmt.Errorf("failed to create KMS crypto key client: %w", err) - } - - kmsCryptoKeyVersionCli, err = kms.NewKeyManagementClient(ctx, opts...) - if err != nil { - return nil, fmt.Errorf("failed to create KMS crypto key version client: %w", err) - } - // Extract project ID from projectLocations for BigQuery client initialization. // // IMPORTANT: The project ID passed to bigquery.NewClient() is used for: @@ -196,6 +183,13 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati return nil, fmt.Errorf("failed to create bigquery client: %w", err) } + // Create KMS asset loader (uses Cloud Asset API for bulk loading) + httpClient, err := shared.GCPHTTPClientWithOtel(ctx, "") + if err != nil { + return nil, fmt.Errorf("failed to create HTTP client for KMS loader: %w", err) + } + kmsLoader = shared.NewCloudKMSAssetLoader(httpClient, bigQueryProjectID, cache, "gcp-source", projectLocations) + loggingConfigCli, err = logging.NewConfigClient(ctx, opts...) if err != nil { return nil, fmt.Errorf("failed to create logging config client: %w", err) @@ -230,6 +224,7 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati sources.WrapperToAdapter(NewComputeAddress(shared.NewComputeAddressClient(addressCli), regionLocations), cache), sources.WrapperToAdapter(NewComputeForwardingRule(shared.NewComputeForwardingRuleClient(computeForwardingCli), regionLocations), cache), sources.WrapperToAdapter(NewComputeNodeTemplate(shared.NewComputeNodeTemplateClient(nodeTemplateCli), regionLocations), cache), + sources.WrapperToAdapter(NewComputeRegionInstanceGroupManager(shared.NewRegionInstanceGroupManagerClient(regionInstanceGroupManagerCli), regionLocations), cache), ) } @@ -280,9 +275,9 @@ 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(NewCloudKMSKeyRing(shared.NewCloudKMSKeyRingClient(kmsKeyRingCli), projectLocations), cache), - sources.WrapperToAdapter(NewCloudKMSCryptoKey(shared.NewCloudKMSCryptoKeyClient(kmsCryptoKeyCli), projectLocations), cache), - sources.WrapperToAdapter(NewCloudKMSCryptoKeyVersion(shared.NewCloudKMSCryptoKeyVersionClient(kmsCryptoKeyVersionCli), 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(NewLoggingSink(shared.NewLoggingConfigClient(loggingConfigCli), projectLocations), cache), sources.WrapperToAdapter(NewBigQueryRoutine(shared.NewBigQueryRoutineClient(bigQueryDatasetCli), projectLocations), cache), diff --git a/sources/gcp/manual/cloud-kms-crypto-key-version.go b/sources/gcp/manual/cloud-kms-crypto-key-version.go index a2e5329a..bfd7e84f 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version.go @@ -2,11 +2,6 @@ package manual import ( "context" - "errors" - "fmt" - - "cloud.google.com/go/kms/apiv1/kmspb" - "google.golang.org/api/iterator" "github.com/overmindtech/cli/discovery" "github.com/overmindtech/cli/sdp-go" @@ -18,17 +13,17 @@ import ( var CloudKMSCryptoKeyVersionLookupByVersion = shared.NewItemTypeLookup("version", gcpshared.CloudKMSCryptoKeyVersion) -// cloudKMSCryptoKeyVersionWrapper wraps the KMS CryptoKeyVersion client for SDP adaptation. +// cloudKMSCryptoKeyVersionWrapper wraps the KMS CryptoKeyVersion operations using CloudKMSAssetLoader. type cloudKMSCryptoKeyVersionWrapper struct { - client gcpshared.CloudKMSCryptoKeyVersionClient + loader *gcpshared.CloudKMSAssetLoader *gcpshared.ProjectBase } // NewCloudKMSCryptoKeyVersion creates a new cloudKMSCryptoKeyVersionWrapper. -func NewCloudKMSCryptoKeyVersion(client gcpshared.CloudKMSCryptoKeyVersionClient, locations []gcpshared.LocationInfo) sources.SearchStreamableWrapper { +func NewCloudKMSCryptoKeyVersion(loader *gcpshared.CloudKMSAssetLoader, locations []gcpshared.LocationInfo) sources.SearchStreamableWrapper { return &cloudKMSCryptoKeyVersionWrapper{ - client: client, + loader: loader, ProjectBase: gcpshared.NewProjectBase( locations, sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, @@ -39,13 +34,12 @@ func NewCloudKMSCryptoKeyVersion(client gcpshared.CloudKMSCryptoKeyVersionClient func (c cloudKMSCryptoKeyVersionWrapper) IAMPermissions() []string { return []string{ - "cloudkms.cryptoKeyVersions.get", - "cloudkms.cryptoKeyVersions.list", + "cloudasset.assets.listResource", } } func (c cloudKMSCryptoKeyVersionWrapper) PredefinedRole() string { - return "roles/cloudkms.viewer" + return "roles/cloudasset.viewer" } // PotentialLinks returns the potential links for the CryptoKeyVersion wrapper. @@ -77,11 +71,10 @@ func (c cloudKMSCryptoKeyVersionWrapper) GetLookups() sources.ItemTypeLookups { } } -// Get retrieves a KMS CryptoKeyVersion by its name. -// The name must be in the format: projects/{PROJECT_ID}/locations/{LOCATION}/keyRings/{KEY_RING}/cryptoKeys/{CRYPTO_KEY}/cryptoKeyVersions/{VERSION} -// See: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys.cryptoKeyVersions/get +// Get retrieves a KMS CryptoKeyVersion by its unique attribute (location|keyRing|cryptoKey|version). +// Data is loaded via Cloud Asset API and cached in sdpcache. func (c cloudKMSCryptoKeyVersionWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - loc, err := c.LocationFromScope(scope) + _, err := c.LocationFromScope(scope) if err != nil { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -89,25 +82,8 @@ func (c cloudKMSCryptoKeyVersionWrapper) Get(ctx context.Context, scope string, } } - location := queryParts[0] - keyRing := queryParts[1] - cryptoKey := queryParts[2] - version := queryParts[3] - - name := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/%s", - loc.ProjectID, location, keyRing, cryptoKey, version, - ) - - req := &kmspb.GetCryptoKeyVersionRequest{ - Name: name, - } - - cryptoKeyVersion, getErr := c.client.Get(ctx, req) - if getErr != nil { - return nil, gcpshared.QueryError(getErr, scope, c.Type()) - } - - return c.gcpCryptoKeyVersionToSDPItem(cryptoKeyVersion, loc) + uniqueAttr := shared.CompositeLookupKey(queryParts...) + return c.loader.GetItem(ctx, scope, c.Type(), uniqueAttr) } // SearchLookups returns the lookups for the CryptoKeyVersion wrapper. @@ -121,16 +97,17 @@ func (c cloudKMSCryptoKeyVersionWrapper) SearchLookups() []sources.ItemTypeLooku } } -// Search searches KMS CryptoKeyVersions for a given CryptoKey and converts them to sdp.Items. -// GET https://cloudkms.googleapis.com/v1/{parent=projects/*/locations/*/keyRings/*/cryptoKeys/*}/cryptoKeyVersions +// Search searches KMS CryptoKeyVersions by cryptoKey (location|keyRing|cryptoKey). +// Data is loaded via Cloud Asset API and cached in sdpcache. func (c cloudKMSCryptoKeyVersionWrapper) 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...) }) } -func (c cloudKMSCryptoKeyVersionWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { - loc, err := c.LocationFromScope(scope) +// SearchStream streams CryptoKeyVersions matching the search criteria (location|keyRing|cryptoKey). +func (c cloudKMSCryptoKeyVersionWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, _ sdpcache.Cache, _ sdpcache.CacheKey, scope string, queryParts ...string) { + _, err := c.LocationFromScope(scope) if err != nil { stream.SendError(&sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -139,168 +116,7 @@ func (c cloudKMSCryptoKeyVersionWrapper) SearchStream(ctx context.Context, strea return } - location := queryParts[0] - keyRing := queryParts[1] - cryptoKey := queryParts[2] - - parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", - loc.ProjectID, location, keyRing, cryptoKey, - ) - - it := c.client.List(ctx, &kmspb.ListCryptoKeyVersionsRequest{ - Parent: parent, - }) - - for { - cryptoKeyVersion, iterErr := it.Next() - if errors.Is(iterErr, iterator.Done) { - break - } - if iterErr != nil { - stream.SendError(gcpshared.QueryError(iterErr, scope, c.Type())) - return - } - - item, sdpErr := c.gcpCryptoKeyVersionToSDPItem(cryptoKeyVersion, loc) - if sdpErr != nil { - stream.SendError(sdpErr) - continue - } - - cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) - stream.SendItem(item) - } -} - -// gcpCryptoKeyVersionToSDPItem converts a GCP CryptoKeyVersion to an SDP Item, linking GCP resource fields. -// See: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys.cryptoKeyVersions -func (c cloudKMSCryptoKeyVersionWrapper) gcpCryptoKeyVersionToSDPItem(cryptoKeyVersion *kmspb.CryptoKeyVersion, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { - attributes, err := shared.ToAttributesWithExclude(cryptoKeyVersion) - if err != nil { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: err.Error(), - } - } - - // The unique attribute must be the same as the query parameter for the Get method. - // Which is in the format: location|keyRing|cryptoKey|version - // We will extract the path parameters from the CryptoKeyVersion name to create a unique lookup key. - // - // Example CryptoKeyVersion name: projects/{PROJECT_ID}/locations/{LOCATION}/keyRings/{KEY_RING}/cryptoKeys/{CRYPTO_KEY}/cryptoKeyVersions/{VERSION} - values := gcpshared.ExtractPathParams(cryptoKeyVersion.GetName(), "locations", "keyRings", "cryptoKeys", "cryptoKeyVersions") - if len(values) != 4 || values[0] == "" || values[1] == "" || values[2] == "" || values[3] == "" { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("invalid CryptoKeyVersion name: %s", cryptoKeyVersion.GetName()), - } - } - - err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(values...)) - if err != nil { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("failed to set unique attribute: %v", err), - } - } - - sdpItem := &sdp.Item{ - Type: gcpshared.CloudKMSCryptoKeyVersion.String(), - UniqueAttribute: "uniqueAttr", - Attributes: attributes, - Scope: location.ToScope(), - } - - // Link to parent CryptoKey - // GET https://cloudkms.googleapis.com/v1/{name=projects/*/locations/*/keyRings/*/cryptoKeys/*} - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/get - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSCryptoKey.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(values[0], values[1], values[2]), // location, keyRing, cryptoKey - Scope: location.ProjectID, - }, - // Deleting the parent CryptoKey deletes all CryptoKeyVersions - // Deleting a CryptoKeyVersion doesn't affect the parent CryptoKey - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - - // Link to ImportJob if the key material was imported - // GET https://cloudkms.googleapis.com/v1/{name=projects/*/locations/*/keyRings/*/importJobs/*} - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.importJobs/get - if importJob := cryptoKeyVersion.GetImportJob(); importJob != "" { - importJobVals := gcpshared.ExtractPathParams(importJob, "locations", "keyRings", "importJobs") - if len(importJobVals) == 3 && importJobVals[0] != "" && importJobVals[1] != "" && importJobVals[2] != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSImportJob.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(importJobVals...), - Scope: location.ProjectID, - }, - // Deleting the ImportJob doesn't affect the CryptoKeyVersion once imported - // The CryptoKeyVersion doesn't own the ImportJob - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } - } - - // Link to EKMConnection if using external key management - // GET https://cloudkms.googleapis.com/v1/{name=projects/*/locations/*/ekmConnections/*} - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.ekmConnections/get - if protectionLevel := cryptoKeyVersion.GetProtectionLevel(); protectionLevel == kmspb.ProtectionLevel_EXTERNAL_VPC { - if externalProtection := cryptoKeyVersion.GetExternalProtectionLevelOptions(); externalProtection != nil { - if ekmPath := externalProtection.GetEkmConnectionKeyPath(); ekmPath != "" { - // Extract EKM connection name from the key path - // EkmConnectionKeyPath format may vary, need to extract connection name carefully - // For now, we'll attempt to parse it if it follows a standard pattern - ekmVals := gcpshared.ExtractPathParams(ekmPath, "locations", "ekmConnections") - if len(ekmVals) == 2 && ekmVals[0] != "" && ekmVals[1] != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSEKMConnection.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(ekmVals...), - Scope: location.ProjectID, - }, - // Deleting the EKM connection makes the CryptoKeyVersion non-functional - // The CryptoKeyVersion doesn't own the EKM connection - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } - } - } - } - - // Set health based on CryptoKeyVersion state - switch cryptoKeyVersion.GetState() { - case kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_STATE_UNSPECIFIED: - sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() - case kmspb.CryptoKeyVersion_PENDING_GENERATION, kmspb.CryptoKeyVersion_PENDING_IMPORT: - sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() - case kmspb.CryptoKeyVersion_ENABLED: - sdpItem.Health = sdp.Health_HEALTH_OK.Enum() - case kmspb.CryptoKeyVersion_DISABLED: - sdpItem.Health = sdp.Health_HEALTH_WARNING.Enum() - case kmspb.CryptoKeyVersion_DESTROYED, kmspb.CryptoKeyVersion_DESTROY_SCHEDULED: - sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() - case kmspb.CryptoKeyVersion_IMPORT_FAILED, kmspb.CryptoKeyVersion_GENERATION_FAILED, kmspb.CryptoKeyVersion_EXTERNAL_DESTRUCTION_FAILED: - sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() - case kmspb.CryptoKeyVersion_PENDING_EXTERNAL_DESTRUCTION: - sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() - default: - sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() - } - - return sdpItem, nil + // CryptoKeyVersion search is by location|keyRing|cryptoKey + searchQuery := shared.CompositeLookupKey(queryParts[0], queryParts[1], queryParts[2]) + c.loader.SearchItems(ctx, stream, scope, c.Type(), searchQuery) } 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 fc50ac65..bb8821e4 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go @@ -2,281 +2,202 @@ package manual_test import ( "context" - "fmt" - "sync" + "errors" "testing" - "cloud.google.com/go/kms/apiv1/kmspb" - "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/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" ) func TestCloudKMSCryptoKeyVersion(t *testing.T) { ctx := context.Background() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := mocks.NewMockCloudKMSCryptoKeyVersionClient(ctrl) projectID := "test-project-id" - location := "us" - keyRingName := "test-keyring" - cryptoKeyName := "test-key" - t.Run("Get", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKeyVersion(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + t.Run("Get_CacheHit", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Pre-populate cache with a CryptoKeyVersion item + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", + "uniqueAttr": "us|test-keyring|test-key|1", + "state": "ENABLED", + }) + _ = attrs.Set("uniqueAttr", "us|test-keyring|test-key|1") + + item := &sdp.Item{ + Type: gcpshared.CloudKMSCryptoKeyVersion.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: projectID, + Health: sdp.Health_HEALTH_OK.Enum(), + } + + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), "us|test-keyring|test-key|1") + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) - mockClient.EXPECT().Get(ctx, gomock.Any()).Return( - createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "1", kmspb.CryptoKeyVersion_ENABLED), - nil, - ) + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + wrapper := manual.NewCloudKMSCryptoKeyVersion(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey(location, keyRingName, cryptoKeyName, "1"), true) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "us|test-keyring|test-key|1", false) if qErr != nil { t.Fatalf("Expected no error, got: %v", qErr) } - if sdpItem.GetHealth() != sdp.Health_HEALTH_OK { - t.Fatalf("Expected health OK for ENABLED version, got: %v", sdpItem.GetHealth()) + if sdpItem == nil { + t.Fatalf("Expected item, got nil") } - t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - { - ExpectedType: gcpshared.CloudKMSCryptoKey.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "us|test-keyring|test-key", - ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + uniqueAttr, err := sdpItem.GetAttributes().Get("uniqueAttr") + if err != nil { + t.Fatalf("Failed to get uniqueAttr: %v", err) + } + if uniqueAttr != "us|test-keyring|test-key|1" { + t.Fatalf("Expected uniqueAttr 'us|test-keyring|test-key|1', got: %v", uniqueAttr) + } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) + // Verify health + if sdpItem.GetHealth() != sdp.Health_HEALTH_OK { + t.Fatalf("Expected health HEALTH_OK, got: %v", sdpItem.GetHealth()) + } }) - t.Run("Get_WithImportJob", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKeyVersion(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + t.Run("Get_CacheMiss_NotFound", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() - versionWithImport := createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "2", kmspb.CryptoKeyVersion_ENABLED) - versionWithImport.ImportJob = "projects/test-project-id/locations/us/keyRings/test-keyring/importJobs/test-import-job" + // Pre-populate cache with a NOTFOUND error to simulate item not existing + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "No resources found in Cloud Asset API", + } + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), "us|test-keyring|test-key|99") + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) - mockClient.EXPECT().Get(ctx, gomock.Any()).Return(versionWithImport, nil) + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey(location, keyRingName, cryptoKeyName, "2"), true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } + wrapper := manual.NewCloudKMSCryptoKeyVersion(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) - // Verify ImportJob link is present - foundImportJobLink := false - for _, link := range sdpItem.GetLinkedItemQueries() { - if link.GetQuery().GetType() == gcpshared.CloudKMSImportJob.String() { - foundImportJobLink = true - expectedQuery := "us|test-keyring|test-import-job" - if link.GetQuery().GetQuery() != expectedQuery { - t.Fatalf("Expected ImportJob query '%s', got: %s", expectedQuery, link.GetQuery().GetQuery()) - } - } + // Get a non-existent item - should return NOTFOUND from cache + _, err := adapter.Get(ctx, wrapper.Scopes()[0], "us|test-keyring|test-key|99", false) + if err == nil { + t.Fatalf("Expected NOTFOUND error, got nil") } - if !foundImportJobLink { - t.Fatalf("Expected ImportJob link to be present") + var qErr *sdp.QueryError + if !errors.As(err, &qErr) { + t.Fatalf("Expected QueryError, got: %T - %v", err, err) } - }) - - t.Run("Get_WithEKMConnection", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKeyVersion(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - versionWithEKM := createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "3", kmspb.CryptoKeyVersion_ENABLED) - versionWithEKM.ProtectionLevel = kmspb.ProtectionLevel_EXTERNAL_VPC - versionWithEKM.ExternalProtectionLevelOptions = &kmspb.ExternalProtectionLevelOptions{ - EkmConnectionKeyPath: "projects/test-project-id/locations/us/ekmConnections/test-ekm-connection", + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("Expected NOTFOUND error type, got: %v", qErr.GetErrorType()) } + }) - mockClient.EXPECT().Get(ctx, gomock.Any()).Return(versionWithEKM, nil) + t.Run("Search_CacheHit", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey(location, keyRingName, cryptoKeyName, "3"), true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } + // Pre-populate cache with CryptoKeyVersion items under SEARCH cache key (by cryptoKey) + attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", + "uniqueAttr": "us|test-keyring|test-key|1", + }) + _ = attrs1.Set("uniqueAttr", "us|test-keyring|test-key|1") - // Verify EKM connection link is present - foundEKMLink := false - for _, link := range sdpItem.GetLinkedItemQueries() { - if link.GetQuery().GetType() == gcpshared.CloudKMSEKMConnection.String() { - foundEKMLink = true - expectedQuery := "us|test-ekm-connection" - if link.GetQuery().GetQuery() != expectedQuery { - t.Fatalf("Expected EKM connection query '%s', got: %s", expectedQuery, link.GetQuery().GetQuery()) - } - } + attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/2", + "uniqueAttr": "us|test-keyring|test-key|2", + }) + _ = attrs2.Set("uniqueAttr", "us|test-keyring|test-key|2") + + item1 := &sdp.Item{ + Type: gcpshared.CloudKMSCryptoKeyVersion.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs1, + Scope: projectID, + Health: sdp.Health_HEALTH_OK.Enum(), } - if !foundEKMLink { - t.Fatalf("Expected EKM connection link to be present") + item2 := &sdp.Item{ + Type: gcpshared.CloudKMSCryptoKeyVersion.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs2, + Scope: projectID, + Health: sdp.Health_HEALTH_WARNING.Enum(), } - }) - t.Run("Get_HealthStates", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKeyVersion(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - testCases := []struct { - state kmspb.CryptoKeyVersion_CryptoKeyVersionState - expectedHealth sdp.Health - }{ - {kmspb.CryptoKeyVersion_ENABLED, sdp.Health_HEALTH_OK}, - {kmspb.CryptoKeyVersion_DISABLED, sdp.Health_HEALTH_WARNING}, - {kmspb.CryptoKeyVersion_DESTROYED, sdp.Health_HEALTH_ERROR}, - {kmspb.CryptoKeyVersion_DESTROY_SCHEDULED, sdp.Health_HEALTH_ERROR}, - {kmspb.CryptoKeyVersion_PENDING_GENERATION, sdp.Health_HEALTH_PENDING}, - {kmspb.CryptoKeyVersion_PENDING_IMPORT, sdp.Health_HEALTH_PENDING}, - {kmspb.CryptoKeyVersion_IMPORT_FAILED, sdp.Health_HEALTH_ERROR}, - {kmspb.CryptoKeyVersion_GENERATION_FAILED, sdp.Health_HEALTH_ERROR}, - } + // Search by location|keyRing|cryptoKey + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), "us|test-keyring|test-key") + cache.StoreItem(ctx, item1, shared.DefaultCacheDuration, searchCacheKey) + cache.StoreItem(ctx, item2, shared.DefaultCacheDuration, searchCacheKey) - for _, tc := range testCases { - t.Run(tc.state.String(), func(t *testing.T) { - mockClient.EXPECT().Get(ctx, gomock.Any()).Return( - createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "1", tc.state), - nil, - ) - - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey(location, keyRingName, cryptoKeyName, "1"), true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - if sdpItem.GetHealth() != tc.expectedHealth { - t.Fatalf("Expected health %v for state %v, got: %v", tc.expectedHealth, tc.state, sdpItem.GetHealth()) - } - }) - } - }) + 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) - t.Run("Search", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKeyVersion(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - mockIterator := mocks.NewMockCloudKMSCryptoKeyVersionIterator(ctrl) - - mockIterator.EXPECT().Next().Return( - createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "1", kmspb.CryptoKeyVersion_ENABLED), - nil, - ) - mockIterator.EXPECT().Next().Return( - createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "2", kmspb.CryptoKeyVersion_DISABLED), - nil, - ) - mockIterator.EXPECT().Next().Return( - createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "3", kmspb.CryptoKeyVersion_DESTROYED), - nil, - ) - mockIterator.EXPECT().Next().Return(nil, iterator.Done) - - mockClient.EXPECT().List(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], shared.CompositeLookupKey(location, keyRingName, cryptoKeyName), true) - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - expectedCount := 3 - actualCount := len(sdpItems) - if actualCount != expectedCount { - t.Fatalf("Expected %d items, got: %d", expectedCount, actualCount) + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], "us|test-keyring|test-key", false) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) } - for _, item := range sdpItems { - if item.Validate() != nil { - t.Fatalf("Expected no validation error, got: %v", item.Validate()) - } + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) } }) - t.Run("SearchStream", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKeyVersion(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - mockIterator := mocks.NewMockCloudKMSCryptoKeyVersionIterator(ctrl) + t.Run("Search_CacheHit_Empty", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() - mockIterator.EXPECT().Next().Return( - createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "1", kmspb.CryptoKeyVersion_ENABLED), - nil, - ) - mockIterator.EXPECT().Next().Return( - createCryptoKeyVersion(projectID, location, keyRingName, cryptoKeyName, "2", kmspb.CryptoKeyVersion_DISABLED), - nil, - ) - mockIterator.EXPECT().Next().Return(nil, iterator.Done) - - mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIterator) + // Store NOTFOUND error in cache to simulate empty result + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "No resources found in Cloud Asset API", + } + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), "us|test-keyring|empty-key") + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, searchCacheKey) - var items []*sdp.Item - var errs []error - wg := &sync.WaitGroup{} - wg.Add(2) + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - mockItemHandler := func(item *sdp.Item) { - items = append(items, item) - wg.Done() - } - mockErrorHandler := func(err error) { - errs = append(errs, err) - } + wrapper := manual.NewCloudKMSCryptoKeyVersion(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) - stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) - // Check if adapter supports search streaming - searchStreamable, ok := adapter.(discovery.SearchStreamableAdapter) + searchable, ok := adapter.(discovery.SearchableAdapter) if !ok { - t.Fatalf("Adapter does not support SearchStream operation") + t.Fatalf("Adapter does not support Search operation") } - searchStreamable.SearchStream(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey(location, keyRingName, cryptoKeyName), 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)) - } - for _, item := range items { - if item.Validate() != nil { - t.Fatalf("Expected no validation error, got: %v", item.Validate()) - } + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], "us|test-keyring|empty-key", false) + if qErr != nil { + t.Fatalf("Expected no error (empty search is valid), got: %v", qErr) } - // Verify adapter does not support ListStream - _, ok = adapter.(discovery.ListStreamableAdapter) - if ok { - t.Fatalf("Adapter should not support ListStream operation") + // Empty result is valid for SEARCH - should return empty slice, not error + if len(items) != 0 { + t.Fatalf("Expected 0 items (empty result), got: %d", len(items)) } }) t.Run("List_Unsupported", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKeyVersion(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + 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) // Check if adapter supports list - it should not _, ok := adapter.(discovery.ListableAdapter) @@ -284,15 +205,66 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { t.Fatalf("Expected adapter to not support List operation, but it does") } }) -} -// createCryptoKeyVersion creates a CryptoKeyVersion with the specified parameters. -func createCryptoKeyVersion(projectID, location, keyRing, cryptoKey, version string, state kmspb.CryptoKeyVersion_CryptoKeyVersionState) *kmspb.CryptoKeyVersion { - return &kmspb.CryptoKeyVersion{ - Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/%s", - projectID, location, keyRing, cryptoKey, version), - State: state, - ProtectionLevel: kmspb.ProtectionLevel_SOFTWARE, - Algorithm: kmspb.CryptoKeyVersion_GOOGLE_SYMMETRIC_ENCRYPTION, - } + t.Run("StaticTests", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Pre-populate cache with a CryptoKeyVersion item with linked queries + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", + "uniqueAttr": "us|test-keyring|test-key|1", + }) + _ = attrs.Set("uniqueAttr", "us|test-keyring|test-key|1") + + item := &sdp.Item{ + Type: gcpshared.CloudKMSCryptoKeyVersion.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: projectID, + Health: sdp.Health_HEALTH_OK.Enum(), + LinkedItemQueries: []*sdp.LinkedItemQuery{ + { + Query: &sdp.Query{ + Type: gcpshared.CloudKMSCryptoKey.String(), + Method: sdp.QueryMethod_GET, + Query: "us|test-keyring|test-key", + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + }, + } + + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), "us|test-keyring|test-key|1") + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + + 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) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "us|test-keyring|test-key|1", false) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.CloudKMSCryptoKey.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "us|test-keyring|test-key", + ExpectedScope: "test-project-id", + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) } diff --git a/sources/gcp/manual/cloud-kms-crypto-key.go b/sources/gcp/manual/cloud-kms-crypto-key.go index 18cb2223..1dbd1eeb 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key.go +++ b/sources/gcp/manual/cloud-kms-crypto-key.go @@ -2,11 +2,6 @@ package manual import ( "context" - "errors" - "fmt" - - "cloud.google.com/go/kms/apiv1/kmspb" - "google.golang.org/api/iterator" "github.com/overmindtech/cli/discovery" "github.com/overmindtech/cli/sdp-go" @@ -18,17 +13,17 @@ import ( var CloudKMSCryptoKeyLookupByName = shared.NewItemTypeLookup("name", gcpshared.CloudKMSCryptoKey) -// cloudKMSCryptoKeyWrapper wraps the KMS CryptoKey client for SDP adaptation. +// cloudKMSCryptoKeyWrapper wraps the KMS CryptoKey operations using CloudKMSAssetLoader. type cloudKMSCryptoKeyWrapper struct { - client gcpshared.CloudKMSCryptoKeyClient + loader *gcpshared.CloudKMSAssetLoader *gcpshared.ProjectBase } // NewCloudKMSCryptoKey creates a new cloudKMSCryptoKeyWrapper. -func NewCloudKMSCryptoKey(client gcpshared.CloudKMSCryptoKeyClient, locations []gcpshared.LocationInfo) sources.SearchStreamableWrapper { +func NewCloudKMSCryptoKey(loader *gcpshared.CloudKMSAssetLoader, locations []gcpshared.LocationInfo) sources.SearchStreamableWrapper { return &cloudKMSCryptoKeyWrapper{ - client: client, + loader: loader, ProjectBase: gcpshared.NewProjectBase( locations, sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, @@ -39,13 +34,12 @@ func NewCloudKMSCryptoKey(client gcpshared.CloudKMSCryptoKeyClient, locations [] func (c cloudKMSCryptoKeyWrapper) IAMPermissions() []string { return []string{ - "cloudkms.cryptoKeys.get", - "cloudkms.cryptoKeys.list", + "cloudasset.assets.listResource", } } func (c cloudKMSCryptoKeyWrapper) PredefinedRole() string { - return "roles/cloudkms.viewer" + return "roles/cloudasset.viewer" } // PotentialLinks returns the potential links for the CryptoKey wrapper. @@ -75,11 +69,10 @@ func (c cloudKMSCryptoKeyWrapper) GetLookups() sources.ItemTypeLookups { } } -// Get retrieves a KMS CryptoKey by its name. -// The name must be in the format: projects/{PROJECT_ID}/locations/{LOCATION}/keyRings/{KEY_RING}/cryptoKeys/{CRYPTO_KEY} -// See: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/get +// Get retrieves a KMS CryptoKey by its unique attribute (location|keyRing|cryptoKeyName). +// Data is loaded via Cloud Asset API and cached in sdpcache. func (c cloudKMSCryptoKeyWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - loc, err := c.LocationFromScope(scope) + _, err := c.LocationFromScope(scope) if err != nil { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -87,24 +80,8 @@ func (c cloudKMSCryptoKeyWrapper) Get(ctx context.Context, scope string, queryPa } } - location := queryParts[0] - keyRing := queryParts[1] - cryptoKeyName := queryParts[2] - - name := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", - loc.ProjectID, location, keyRing, cryptoKeyName, - ) - - req := &kmspb.GetCryptoKeyRequest{ - Name: name, - } - - cryptoKey, getErr := c.client.Get(ctx, req) - if getErr != nil { - return nil, gcpshared.QueryError(getErr, scope, c.Type()) - } - - return c.gcpCryptoKeyToSDPItem(cryptoKey, loc) + uniqueAttr := shared.CompositeLookupKey(queryParts...) + return c.loader.GetItem(ctx, scope, c.Type(), uniqueAttr) } // SearchLookups returns the lookups for the CryptoKey wrapper. @@ -117,16 +94,17 @@ func (c cloudKMSCryptoKeyWrapper) SearchLookups() []sources.ItemTypeLookups { } } -// Search searches KMS CryptoKeys and converts them to sdp.Items. -// GET https://cloudkms.googleapis.com/v1/{parent=projects/*/locations/*/keyRings/*}/cryptoKeys +// Search searches KMS CryptoKeys by keyRing (location|keyRing). +// Data is loaded via Cloud Asset API and cached in sdpcache. func (c cloudKMSCryptoKeyWrapper) 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...) }) } -func (c cloudKMSCryptoKeyWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { - loc, err := c.LocationFromScope(scope) +// SearchStream streams CryptoKeys matching the search criteria (location|keyRing). +func (c cloudKMSCryptoKeyWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, _ sdpcache.Cache, _ sdpcache.CacheKey, scope string, queryParts ...string) { + _, err := c.LocationFromScope(scope) if err != nil { stream.SendError(&sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -135,211 +113,7 @@ func (c cloudKMSCryptoKeyWrapper) SearchStream(ctx context.Context, stream disco return } - location := queryParts[0] - keyRing := queryParts[1] - - parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", - loc.ProjectID, location, keyRing, - ) - - it := c.client.List(ctx, &kmspb.ListCryptoKeysRequest{ - Parent: parent, - }) - - for { - cryptoKey, iterErr := it.Next() - if errors.Is(iterErr, iterator.Done) { - break - } - if iterErr != nil { - stream.SendError(gcpshared.QueryError(iterErr, scope, c.Type())) - return - } - - item, sdpErr := c.gcpCryptoKeyToSDPItem(cryptoKey, loc) - if sdpErr != nil { - stream.SendError(sdpErr) - continue - } - - cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) - stream.SendItem(item) - } -} - -// gcpCryptoKeyToSDPItem converts a GCP CryptoKey to an SDP Item, linking GCP resource fields. -// See: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys -func (c cloudKMSCryptoKeyWrapper) gcpCryptoKeyToSDPItem(cryptoKey *kmspb.CryptoKey, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { - attributes, err := shared.ToAttributesWithExclude(cryptoKey, "labels") - if err != nil { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: err.Error(), - } - } - - // The unique attribute must be the same as the query parameter for the Get method. - // Which is in the format: locations|keyRingName|cryptoKeyName - // We will extract the path parameters from the CryptoKey name to create a unique lookup key. - // - // [CryptoKey][google.cloud.kms.v1.CryptoKey] in the format - // `projects/*/locations/*/keyRings/*/cryptoKeys/*`. - values := gcpshared.ExtractPathParams(cryptoKey.GetName(), "locations", "keyRings", "cryptoKeys") - kmsLocation := values[0] - keyRing := values[1] - cryptoKeyName := values[2] - if len(values) != 3 { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("invalid CryptoKey name: %s", cryptoKey.GetName()), - } - } - - err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(values...)) - if err != nil { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("failed to set unique attribute: %v", err), - } - } - - sdpItem := &sdp.Item{ - Type: gcpshared.CloudKMSCryptoKey.String(), - UniqueAttribute: "uniqueAttr", - Attributes: attributes, - Scope: location.ToScope(), - Tags: cryptoKey.GetLabels(), - } - - // The IAM policy associated with this CryptoKey. - // GET https://cloudkms.googleapis.com/v1/{resource=projects/*/locations/*/keyRings/*/cryptoKeys/*}:getIamPolicy - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/getIamPolicy - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.IAMPolicy.String(), - Method: sdp.QueryMethod_GET, - // TODO(Nauany): "":getIamPolicy" needs to be appended at the end of the URL, ensure team is aware - Query: shared.CompositeLookupKey(kmsLocation, keyRing, cryptoKeyName), - Scope: location.ProjectID, - }, - // Deleting the IAM Policy makes the CryptoKey non-functional - // Deleting the CryptoKey deletes the IAM Policy - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }) - - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSKeyRing.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(kmsLocation, keyRing), - Scope: location.ProjectID, - }, - // Deleting the KeyRing makes the CryptoKey non-functional - // Deleting the CryptoKey does not affect the KeyRing; KeyRings are not owned by individual CryptoKeys. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - - // Link to all CryptoKeyVersions for this CryptoKey - // SEARCH https://cloudkms.googleapis.com/v1/{parent=projects/*/locations/*/keyRings/*/cryptoKeys/*}/cryptoKeyVersions - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys.cryptoKeyVersions/list - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSCryptoKeyVersion.String(), - Method: sdp.QueryMethod_SEARCH, - Query: shared.CompositeLookupKey(kmsLocation, keyRing, cryptoKeyName), - Scope: location.ProjectID, - }, - // If all versions of a crypto key are deleted it will become non-functional - // CryptoKey can only be deleted if all versions are deleted - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }) - - // The resource name of the primary CryptoKeyVersion for this CryptoKey. - // GET https://cloudkms.googleapis.com/v1/{name=projects/*/locations/*/keyRings/*/cryptoKeys/*/cryptoKeyVersions/*} - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys.cryptoKeyVersions/get - // Attribute link: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys#:~:text=keyRings/*/cryptoKeys/*.-,primary,-object%20(CryptoKeyVersion - if primary := cryptoKey.GetPrimary(); primary != nil { - if name := primary.GetName(); name != "" { - keyVersionVals := gcpshared.ExtractPathParams(name, "locations", "keyRings", "cryptoKeys", "cryptoKeyVersions") - if len(keyVersionVals) == 4 && keyVersionVals[0] != "" && keyVersionVals[1] != "" && keyVersionVals[2] != "" && keyVersionVals[3] != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSCryptoKeyVersion.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(keyVersionVals...), - Scope: location.ProjectID, - }, - // Primary version is the default for cryptographic operations - // Deleting the primary version requires promoting another version - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }) - } - } - - // The ImportJob resource that was used to import key material for this CryptoKeyVersion. - // Only present if the version was imported (not generated by KMS). - // GET https://cloudkms.googleapis.com/v1/{name=projects/*/locations/*/keyRings/*/importJobs/*} - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.importJobs/get - if importJob := primary.GetImportJob(); importJob != "" { - importJobVals := gcpshared.ExtractPathParams(importJob, "locations", "keyRings", "importJobs") - if len(importJobVals) == 3 && importJobVals[0] != "" && importJobVals[1] != "" && importJobVals[2] != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSImportJob.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(importJobVals...), - Scope: location.ProjectID, - }, - // Deleting the ImportJob makes the CryptoKeyVersion non-functional if it was imported - // Deleting the CryptoKey or CryptoKeyVersion doesn't affect the ImportJob; ImportJobs are not owned by individual CryptoKeys. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } - } - - // The EKM connection used by the key material, if applicable. - // Only applicable if CryptoKeyVersions have a ProtectionLevel of EXTERNAL_VPC. - // Primary is the CryptoKeyVersion that will be used by cryptoKeys.encrypt. - // with the resource name in the format: projects/*/locations/*/ekmConnections/*. - // GET https://cloudkms.googleapis.com/v1/{name=projects/*/locations/*/ekmConnections/*} - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.ekmConnections/get - if protectionLevel := primary.GetProtectionLevel(); protectionLevel == kmspb.ProtectionLevel_EXTERNAL_VPC { - if cryptoKeyBackend := cryptoKey.GetCryptoKeyBackend(); cryptoKeyBackend != "" { - backendVals := gcpshared.ExtractPathParams(cryptoKeyBackend, "locations", "ekmConnections") - if len(backendVals) == 2 && backendVals[0] != "" && backendVals[1] != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSEKMConnection.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(backendVals...), - Scope: location.ProjectID, - }, - // Deleting the CryptoKeyBackend makes the CryptoKey non-functional - // Deleting the CryptoKey doesn't affect the EKMConnection; EKM Connections are not owned by individual CryptoKeys. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } - } - } - } - - return sdpItem, nil + // CryptoKey search is by location|keyRing + searchQuery := shared.CompositeLookupKey(queryParts[0], queryParts[1]) + c.loader.SearchItems(ctx, stream, scope, c.Type(), searchQuery) } diff --git a/sources/gcp/manual/cloud-kms-crypto-key_test.go b/sources/gcp/manual/cloud-kms-crypto-key_test.go index 98a9841f..20948a98 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key_test.go @@ -2,238 +2,199 @@ package manual_test import ( "context" - "fmt" - "sync" + "errors" "testing" - "cloud.google.com/go/kms/apiv1/kmspb" - "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/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" ) func TestCloudKMSCryptoKey(t *testing.T) { ctx := context.Background() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := mocks.NewMockCloudKMSCryptoKeyClient(ctrl) projectID := "test-project-id" - t.Run("Get", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKey(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + t.Run("Get_CacheHit", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() - mockClient.EXPECT().Get(ctx, gomock.Any()).Return( - createCryptoKey( - "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key", - "1", - kmspb.CryptoKeyVersion_ENABLED, - ), nil) + // Pre-populate cache with a CryptoKey item + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key", + "uniqueAttr": "global|test-keyring|test-key", + }) + _ = attrs.Set("uniqueAttr", "global|test-keyring|test-key") + + item := &sdp.Item{ + Type: gcpshared.CloudKMSCryptoKey.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: projectID, + Tags: map[string]string{"env": "test"}, + } - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSCryptoKey.String(), "global|test-keyring|test-key") + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey("location", "keyRing", "cryptoKey"), true) + 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) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "global|test-keyring|test-key", false) if qErr != nil { t.Fatalf("Expected no error, got: %v", qErr) } - expectedTag := "test" - actualTag := sdpItem.GetTags()["env"] - if actualTag != expectedTag { - t.Fatalf("Expected tag 'env=%s', got: %v", expectedTag, actualTag) + if sdpItem == nil { + t.Fatalf("Expected item, got nil") } - t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - { - 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, - }, - }, - { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|1", - ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { - ExpectedType: gcpshared.CloudKMSEKMConnection.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|valid-ekm-connection", - ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { - ExpectedType: gcpshared.IAMPolicy.String(), - 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, - }, - }, - } + uniqueAttr, err := sdpItem.GetAttributes().Get("uniqueAttr") + if err != nil { + t.Fatalf("Failed to get uniqueAttr: %v", err) + } + if uniqueAttr != "global|test-keyring|test-key" { + t.Fatalf("Expected uniqueAttr 'global|test-keyring|test-key', got: %v", uniqueAttr) + } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) + // Verify tags + if sdpItem.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %v", sdpItem.GetTags()) + } }) - t.Run("Search", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKey(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - mockCryptoKeyIterator := mocks.NewMockCloudKMSCryptoKeyIterator(ctrl) - - mockCryptoKeyIterator.EXPECT().Next().Return( - createCryptoKey( - "projects/test-project-id/locations/global/keyRings/test-key-ring/cryptoKeys/test-key-1", - "1", - kmspb.CryptoKeyVersion_ENABLED, - ), nil) - mockCryptoKeyIterator.EXPECT().Next().Return( - createCryptoKey( - "projects/test-project-id/locations/global/keyRings/test-key-ring/cryptoKeys/test-key-2", - "1", - kmspb.CryptoKeyVersion_ENABLED, - ), nil) - // This one is for a different key ring and should be filtered out. - mockCryptoKeyIterator.EXPECT().Next().Return( - createCryptoKey( - "projects/test-project-id/locations/global/keyRings/other-key-ring/cryptoKeys/test-key-3", - "1", - kmspb.CryptoKeyVersion_ENABLED, - ), nil) - mockCryptoKeyIterator.EXPECT().Next().Return(nil, iterator.Done) - - mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockCryptoKeyIterator) - - // [SPEC] Search filters by the key ring. It will list all crypto keys - // any crypto keys that are not using the given key ring. - // Check if adapter supports searching - searchable, ok := adapter.(discovery.SearchableAdapter) - if !ok { - t.Fatalf("Adapter does not support Search operation") - } + t.Run("Get_CacheMiss_NotFound", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() - sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey("location", "key-ring"), true) - if err != nil { - t.Fatalf("Expected no error, got: %v", err) + // Pre-populate cache with a NOTFOUND error to simulate item not existing + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "No resources found in Cloud Asset API", } + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSCryptoKey.String(), "global|test-keyring|nonexistent") + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) - // 2 of 3 are filtered in. - if len(sdpItems) != 3 { - t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) - } + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - for _, item := range sdpItems { - if item.Validate() != nil { - t.Fatalf("Expected no validation error, got: %v", item.Validate()) - } + wrapper := manual.NewCloudKMSCryptoKey(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) - attributes := item.GetAttributes() - _, err := attributes.Get("name") - if err != nil { - t.Fatalf("Failed to get name attribute: %v", err) - } + // Get a non-existent item - should return NOTFOUND from cache + _, err := adapter.Get(ctx, wrapper.Scopes()[0], "global|test-keyring|nonexistent", false) + if err == nil { + t.Fatalf("Expected NOTFOUND error, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) { + t.Fatalf("Expected QueryError, got: %T - %v", err, err) + } + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("Expected NOTFOUND error type, got: %v", qErr.GetErrorType()) } }) - t.Run("SearchStream", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKey(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - mockCryptoKeyIterator := mocks.NewMockCloudKMSCryptoKeyIterator(ctrl) - - // add mock implementation here - mockCryptoKeyIterator.EXPECT().Next().Return( - createCryptoKey( - "projects/test-project-id/locations/global/keyRings/test-key-ring/cryptoKeys/test-key-1", - "1", - kmspb.CryptoKeyVersion_ENABLED, - ), nil) - mockCryptoKeyIterator.EXPECT().Next().Return( - createCryptoKey( - "projects/test-project-id/locations/global/keyRings/test-key-ring/cryptoKeys/test-key-2", - "1", - kmspb.CryptoKeyVersion_ENABLED, - ), nil) - mockCryptoKeyIterator.EXPECT().Next().Return(nil, iterator.Done) - - // Mock the List method - mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockCryptoKeyIterator) - - wg := &sync.WaitGroup{} - wg.Add(2) // we added two items - - var items []*sdp.Item - mockItemHandler := func(item *sdp.Item) { - items = append(items, item) - wg.Done() // signal that we processed an item - } + t.Run("Search_CacheHit", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Pre-populate cache with CryptoKey items under SEARCH cache key (by keyRing) + attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key-1", + "uniqueAttr": "global|test-keyring|test-key-1", + }) + _ = attrs1.Set("uniqueAttr", "global|test-keyring|test-key-1") + + attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key-2", + "uniqueAttr": "global|test-keyring|test-key-2", + }) + _ = attrs2.Set("uniqueAttr", "global|test-keyring|test-key-2") - var errs []error - mockErrorHandler := func(err error) { - errs = append(errs, err) + item1 := &sdp.Item{ + Type: gcpshared.CloudKMSCryptoKey.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs1, + Scope: projectID, } + item2 := &sdp.Item{ + Type: gcpshared.CloudKMSCryptoKey.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs2, + Scope: projectID, + } + + // Search by location|keyRing + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKey.String(), "global|test-keyring") + cache.StoreItem(ctx, item1, shared.DefaultCacheDuration, searchCacheKey) + cache.StoreItem(ctx, item2, shared.DefaultCacheDuration, searchCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) - // Check if adapter supports search streaming - searchStreamable, ok := adapter.(discovery.SearchStreamableAdapter) + wrapper := manual.NewCloudKMSCryptoKey(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + + searchable, ok := adapter.(discovery.SearchableAdapter) if !ok { - t.Fatalf("Adapter does not support SearchStream operation") + t.Fatalf("Adapter does not support Search operation") } - searchStreamable.SearchStream(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey("global", "test-key-ring"), true, stream) - wg.Wait() - - if len(errs) != 0 { - t.Fatalf("Expected no errors, got: %v", errs) + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], "global|test-keyring", false) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) } if len(items) != 2 { t.Fatalf("Expected 2 items, got: %d", len(items)) } + }) - _, ok = adapter.(discovery.ListStreamableAdapter) - if ok { - t.Fatalf("Adapter should not support ListStream operation") + t.Run("Search_CacheHit_Empty", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Store NOTFOUND error in cache to simulate empty result + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "No resources found in Cloud Asset API", + } + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKey.String(), "global|empty-keyring") + 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) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], "global|empty-keyring", false) + if qErr != nil { + t.Fatalf("Expected no error (empty search is valid), got: %v", qErr) + } + + // Empty result is valid for SEARCH - should return empty slice, not error + if len(items) != 0 { + t.Fatalf("Expected 0 items (empty result), got: %d", len(items)) } }) t.Run("List_Unsupported", func(t *testing.T) { - wrapper := manual.NewCloudKMSCryptoKey(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + 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) // Check if adapter supports list - it should not _, ok := adapter.(discovery.ListableAdapter) @@ -241,22 +202,109 @@ func TestCloudKMSCryptoKey(t *testing.T) { t.Fatalf("Expected adapter to not support List operation, but it does") } }) -} -// createCryptoKey creates a CryptoKey with the specified name, primary version, and state. -func createCryptoKey(name, versionNumber string, state kmspb.CryptoKeyVersion_CryptoKeyVersionState) *kmspb.CryptoKey { - var primary *kmspb.CryptoKeyVersion - if versionNumber != "" { - primary = &kmspb.CryptoKeyVersion{ - Name: fmt.Sprintf("%s/cryptoKeyVersions/%s", name, versionNumber), - State: state, - ProtectionLevel: kmspb.ProtectionLevel_EXTERNAL_VPC, + t.Run("StaticTests", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Pre-populate cache with a CryptoKey item with linked queries + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key", + "uniqueAttr": "global|test-keyring|test-key", + }) + _ = attrs.Set("uniqueAttr", "global|test-keyring|test-key") + + item := &sdp.Item{ + Type: gcpshared.CloudKMSCryptoKey.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: projectID, + LinkedItemQueries: []*sdp.LinkedItemQuery{ + { + Query: &sdp.Query{ + Type: gcpshared.IAMPolicy.String(), + Method: sdp.QueryMethod_GET, + Query: "global|test-keyring|test-key", + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + { + Query: &sdp.Query{ + Type: gcpshared.CloudKMSKeyRing.String(), + Method: sdp.QueryMethod_GET, + Query: "global|test-keyring", + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + { + Query: &sdp.Query{ + Type: gcpshared.CloudKMSCryptoKeyVersion.String(), + Method: sdp.QueryMethod_SEARCH, + Query: "global|test-keyring|test-key", + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + }, } - } - return &kmspb.CryptoKey{ - Name: name, - Primary: primary, - CryptoKeyBackend: "projects/test-project-id/locations/global/ekmConnections/valid-ekm-connection", - Labels: map[string]string{"env": "test"}, - } + + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSCryptoKey.String(), "global|test-keyring|test-key") + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + + 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) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "global|test-keyring|test-key", false) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.IAMPolicy.String(), + 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, + }, + }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) } diff --git a/sources/gcp/manual/cloud-kms-key-ring.go b/sources/gcp/manual/cloud-kms-key-ring.go index 9aca4d9e..a5cd48e2 100644 --- a/sources/gcp/manual/cloud-kms-key-ring.go +++ b/sources/gcp/manual/cloud-kms-key-ring.go @@ -2,14 +2,6 @@ package manual import ( "context" - "errors" - "fmt" - "strings" - - "cloud.google.com/go/kms/apiv1/kmspb" - "github.com/sourcegraph/conc/pool" - "google.golang.org/api/iterator" - locationpb "google.golang.org/genproto/googleapis/cloud/location" "github.com/overmindtech/cli/discovery" "github.com/overmindtech/cli/sdp-go" @@ -24,17 +16,17 @@ var ( CloudKMSCryptoKeyRingLookupByLocation = shared.NewItemTypeLookup("location", gcpshared.CloudKMSKeyRing) ) -// cloudKMSKeyRingWrapper wraps the KMS KeyRing client for SDP adaptation. +// cloudKMSKeyRingWrapper wraps the KMS KeyRing operations using CloudKMSAssetLoader. type cloudKMSKeyRingWrapper struct { - client gcpshared.CloudKMSKeyRingClient + loader *gcpshared.CloudKMSAssetLoader *gcpshared.ProjectBase } // NewCloudKMSKeyRing creates a new cloudKMSKeyRingWrapper. -func NewCloudKMSKeyRing(client gcpshared.CloudKMSKeyRingClient, locations []gcpshared.LocationInfo) sources.SearchableListableWrapper { +func NewCloudKMSKeyRing(loader *gcpshared.CloudKMSAssetLoader, locations []gcpshared.LocationInfo) sources.SearchableListableWrapper { return &cloudKMSKeyRingWrapper{ - client: client, + loader: loader, ProjectBase: gcpshared.NewProjectBase( locations, sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, @@ -45,14 +37,12 @@ func NewCloudKMSKeyRing(client gcpshared.CloudKMSKeyRingClient, locations []gcps func (c cloudKMSKeyRingWrapper) IAMPermissions() []string { return []string{ - "cloudkms.keyRings.get", - "cloudkms.keyRings.list", - "cloudkms.locations.list", + "cloudasset.assets.listResource", } } func (c cloudKMSKeyRingWrapper) PredefinedRole() string { - return "roles/cloudkms.viewer" + return "roles/cloudasset.viewer" } // PotentialLinks returns the potential links for the kms key ring @@ -81,11 +71,10 @@ func (c cloudKMSKeyRingWrapper) GetLookups() sources.ItemTypeLookups { } } -// Get retrieves a KMS KeyRing by its name. -// The name must be in the format: projects/{PROJECT_ID}/locations/{LOCATION}/keyRings/{KEY_RING} -// See: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings/get +// Get retrieves a KMS KeyRing by its unique attribute (location|keyRingName). +// Data is loaded via Cloud Asset API and cached in sdpcache. func (c cloudKMSKeyRingWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - loc, err := c.LocationFromScope(scope) + _, err := c.LocationFromScope(scope) if err != nil { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -93,23 +82,8 @@ func (c cloudKMSKeyRingWrapper) Get(ctx context.Context, scope string, queryPart } } - location := queryParts[0] - keyRingName := queryParts[1] - - name := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", - loc.ProjectID, location, keyRingName, - ) - - req := &kmspb.GetKeyRingRequest{ - Name: name, - } - - keyRing, getErr := c.client.Get(ctx, req) - if getErr != nil { - return nil, gcpshared.QueryError(getErr, scope, c.Type()) - } - - return c.gcpKeyRingToSDPItem(keyRing, loc) + uniqueAttr := shared.CompositeLookupKey(queryParts...) + return c.loader.GetItem(ctx, scope, c.Type(), uniqueAttr) } // SearchLookups returns the lookups for the KeyRing wrapper. @@ -121,18 +95,17 @@ func (c cloudKMSKeyRingWrapper) SearchLookups() []sources.ItemTypeLookups { } } -// Search searches KMS KeyRings and converts them to sdp.Items. -// Searchable adapter because location parameter needs to be passed as a queryPart. -// GET https://cloudkms.googleapis.com/v1/{parent=projects/*/locations/*}/keyRings +// Search searches KMS KeyRings by location. +// Data is loaded via Cloud Asset API and cached in sdpcache. func (c cloudKMSKeyRingWrapper) 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 the search results for KMS KeyRings. -func (c cloudKMSKeyRingWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { - loc, err := c.LocationFromScope(scope) +// SearchStream streams KeyRings matching the search criteria (location). +func (c cloudKMSKeyRingWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, _ sdpcache.Cache, _ sdpcache.CacheKey, scope string, queryParts ...string) { + _, err := c.LocationFromScope(scope) if err != nil { stream.SendError(&sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -141,45 +114,22 @@ func (c cloudKMSKeyRingWrapper) SearchStream(ctx context.Context, stream discove return } - parent := fmt.Sprintf("projects/%s/locations/%s", loc.ProjectID, queryParts[0]) - - it := c.client.Search(ctx, &kmspb.ListKeyRingsRequest{ - Parent: parent, - }) - - for { - keyRing, iterErr := it.Next() - if errors.Is(iterErr, iterator.Done) { - break - } - if iterErr != nil { - stream.SendError(gcpshared.QueryError(iterErr, scope, c.Type())) - return - } - - item, sdpErr := c.gcpKeyRingToSDPItem(keyRing, loc) - if sdpErr != nil { - stream.SendError(sdpErr) - continue - } - - cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) - stream.SendItem(item) - } + // KeyRing search is by location only + location := queryParts[0] + c.loader.SearchItems(ctx, stream, scope, c.Type(), location) } -// List lists all KMS KeyRings across all locations in the project. -// It first lists all available KMS locations, then lists key rings from each location in parallel. +// List lists all KMS KeyRings in the project. +// Data is loaded via Cloud Asset API and cached in sdpcache. func (c cloudKMSKeyRingWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { return gcpshared.CollectFromStream(ctx, func(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey) { c.ListStream(ctx, stream, cache, cacheKey, scope) }) } -// ListStream streams all KMS KeyRings across all locations in the project. -// It first lists all available KMS locations, then streams key rings from each location in parallel. -func (c cloudKMSKeyRingWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { - loc, err := c.LocationFromScope(scope) +// ListStream streams all KeyRings in the project. +func (c cloudKMSKeyRingWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, _ sdpcache.Cache, _ sdpcache.CacheKey, scope string) { + _, err := c.LocationFromScope(scope) if err != nil { stream.SendError(&sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -188,123 +138,5 @@ func (c cloudKMSKeyRingWrapper) ListStream(ctx context.Context, stream discovery return } - // List all available KMS locations - parent := fmt.Sprintf("projects/%s", loc.ProjectID) - locationIt := c.client.ListLocations(ctx, &locationpb.ListLocationsRequest{ - Name: parent, - }) - - var locationIDs []string - for { - location, iterErr := locationIt.Next() - if errors.Is(iterErr, iterator.Done) { - break - } - if iterErr != nil { - stream.SendError(gcpshared.QueryError(iterErr, scope, c.Type())) - return - } - - // Extract location ID from the full location name - // Format: projects/{PROJECT_ID}/locations/{LOCATION_ID} - locationName := location.GetName() - parts := strings.Split(locationName, "/") - if len(parts) >= 4 && parts[len(parts)-2] == "locations" { - locationIDs = append(locationIDs, parts[len(parts)-1]) - } - } - - if len(locationIDs) == 0 { - return - } - - // Use SearchStream for each location in parallel - // Use conc pool to limit concurrent goroutines - p := pool.New().WithMaxGoroutines(10).WithContext(ctx) - for _, locationID := range locationIDs { - p.Go(func(ctx context.Context) error { - c.SearchStream(ctx, stream, cache, cacheKey, scope, locationID) - return nil - }) - } - - // Wait for all goroutines to complete - _ = p.Wait() -} - -// gcpKeyRingToSDPItem converts a GCP KeyRing to an SDP Item, linking GCP resource fields. -// See: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings -func (c cloudKMSKeyRingWrapper) gcpKeyRingToSDPItem(keyRing *kmspb.KeyRing, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { - attributes, err := shared.ToAttributesWithExclude(keyRing) - if err != nil { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: err.Error(), - } - } - - // The unique attribute must be the same as the query parameter for the Get method. - // Which is in the format: locations|keyRingName - // We will extract the path parameters from the KeyRing name to create a unique lookup key. - // - // Example KeyRing name: projects/{PROJECT_ID}/locations/{LOCATION}/keyRings/{KEY_RING} - // Unique lookup key: locations|keyRingName - // Extract the keyRingName from the KeyRing name. - keyRingVals := gcpshared.ExtractPathParams(keyRing.GetName(), "locations", "keyRings") - if len(keyRingVals) != 2 || keyRingVals[0] == "" || keyRingVals[1] == "" { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("invalid KeyRing name: %s", keyRing.GetName()), - } - } - - err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(keyRingVals...)) - if err != nil { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("failed to set unique attribute: %v", err), - } - } - - sdpItem := &sdp.Item{ - Type: gcpshared.CloudKMSKeyRing.String(), - UniqueAttribute: "uniqueAttr", - Attributes: attributes, - Scope: location.ToScope(), - } - - // The IAM policy associated with this KeyRing. - // GET https://cloudkms.googleapis.com/v1/{resource=projects/*/locations/*/keyRings/*}:getIamPolicy - // https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings/getIamPolicy - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.IAMPolicy.String(), - Method: sdp.QueryMethod_GET, - // TODO(Nauany): "":getIamPolicy" needs to be appended at the end of the URL, ensure team is aware - Query: shared.CompositeLookupKey(keyRingVals...), - Scope: location.ProjectID, - }, - // Updating the IAM Policy makes the KeyRing non-functional - // KeyRings cannot be deleted or updated - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }) - - // The KMS CryptoKeys associated with this KeyRing. - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSCryptoKey.String(), - Method: sdp.QueryMethod_SEARCH, - Query: shared.CompositeLookupKey(keyRingVals[0], keyRingVals[1]), // location|keyRingName - Scope: location.ProjectID, - }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }) - - return sdpItem, nil + c.loader.ListItems(ctx, stream, scope, c.Type()) } diff --git a/sources/gcp/manual/cloud-kms-key-ring_test.go b/sources/gcp/manual/cloud-kms-key-ring_test.go index 46b15fdc..f4f79a97 100644 --- a/sources/gcp/manual/cloud-kms-key-ring_test.go +++ b/sources/gcp/manual/cloud-kms-key-ring_test.go @@ -2,308 +2,306 @@ package manual_test import ( "context" - "strings" - "sync" + "errors" "testing" - "cloud.google.com/go/kms/apiv1/kmspb" - "go.uber.org/mock/gomock" - "google.golang.org/api/iterator" - locationpb "google.golang.org/genproto/googleapis/cloud/location" - "github.com/overmindtech/cli/discovery" "github.com/overmindtech/cli/sdp-go" "github.com/overmindtech/cli/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" ) func TestCloudKMSKeyRing(t *testing.T) { ctx := context.Background() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := mocks.NewMockCloudKMSKeyRingClient(ctrl) projectID := "test-project-id" - location := "us" - keyRingName := "test-keyring" - t.Run("Get", func(t *testing.T) { - wrapper := manual.NewCloudKMSKeyRing(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + t.Run("Get_CacheHit", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Pre-populate cache with a KeyRing item (simulating what the loader would do) + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring", + "uniqueAttr": "us|test-keyring", + }) + _ = attrs.Set("uniqueAttr", "us|test-keyring") + + item := &sdp.Item{ + Type: gcpshared.CloudKMSKeyRing.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: projectID, + } + + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSKeyRing.String(), "us|test-keyring") + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) - mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createKeyRing(projectID, location, keyRingName), nil) + // Create loader that won't need to make API calls since cache is populated + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + wrapper := manual.NewCloudKMSKeyRing(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], shared.CompositeLookupKey(location, keyRingName), true) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "us|test-keyring", false) if qErr != nil { t.Fatalf("Expected no error, got: %v", qErr) } - t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - { - ExpectedType: gcpshared.IAMPolicy.String(), - 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, - }, - }, - } + if sdpItem == nil { + t.Fatalf("Expected item, got nil") + } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) + uniqueAttr, err := sdpItem.GetAttributes().Get("uniqueAttr") + if err != nil { + t.Fatalf("Failed to get uniqueAttr: %v", err) + } + if uniqueAttr != "us|test-keyring" { + t.Fatalf("Expected uniqueAttr 'us|test-keyring', got: %v", uniqueAttr) + } }) - t.Run("Search", func(t *testing.T) { - wrapper := manual.NewCloudKMSKeyRing(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - mockIterator := mocks.NewMockCloudKMSKeyRingIterator(ctrl) + t.Run("Get_CacheMiss_NotFound", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() - mockIterator.EXPECT().Next().Return(createKeyRing(projectID, location, "test-keyring-1"), nil) - mockIterator.EXPECT().Next().Return(createKeyRing(projectID, location, "test-keyring-2"), nil) - mockIterator.EXPECT().Next().Return(nil, iterator.Done) + // Pre-populate cache with a NOTFOUND error to simulate item not existing + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "No resources found in Cloud Asset API", + } + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSKeyRing.String(), "us|nonexistent") + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) - mockClient.EXPECT().Search(ctx, gomock.Any()).Return(mockIterator) + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - // Check if adapter supports searching - searchable, ok := adapter.(discovery.SearchableAdapter) - if !ok { - t.Fatalf("Adapter does not support Search operation") - } + wrapper := manual.NewCloudKMSKeyRing(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) - sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], location, true) - if err != nil { - t.Fatalf("Expected no error, got: %v", err) + // Get a non-existent item - should return NOTFOUND from cache + _, err := adapter.Get(ctx, wrapper.Scopes()[0], "us|nonexistent", false) + if err == nil { + t.Fatalf("Expected NOTFOUND error, got nil") } - - expectedCount := 2 - actualCount := len(sdpItems) - if actualCount != expectedCount { - t.Fatalf("Expected %d items, got: %d", expectedCount, actualCount) + var qErr *sdp.QueryError + if !errors.As(err, &qErr) { + t.Fatalf("Expected QueryError, got: %T - %v", err, err) } - for _, item := range sdpItems { - if item.Validate() != nil { - t.Fatalf("Expected no validation error, got: %v", item.Validate()) - } + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("Expected NOTFOUND error type, got: %v", qErr.GetErrorType()) } }) - t.Run("SearchStream", func(t *testing.T) { - wrapper := manual.NewCloudKMSKeyRing(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + t.Run("List_CacheHit", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() - mockIterator := mocks.NewMockCloudKMSKeyRingIterator(ctrl) - - mockIterator.EXPECT().Next().Return(createKeyRing(projectID, location, "test-keyring-1"), nil) - mockIterator.EXPECT().Next().Return(createKeyRing(projectID, location, "test-keyring-2"), nil) - mockIterator.EXPECT().Next().Return(nil, iterator.Done) - - mockClient.EXPECT().Search(ctx, gomock.Any()).Return(mockIterator) + // Pre-populate cache with KeyRing items under LIST cache key + attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring-1", + "uniqueAttr": "us|test-keyring-1", + }) + _ = attrs1.Set("uniqueAttr", "us|test-keyring-1") - var items []*sdp.Item - var errs []error - wg := &sync.WaitGroup{} - wg.Add(2) + attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring-2", + "uniqueAttr": "us|test-keyring-2", + }) + _ = attrs2.Set("uniqueAttr", "us|test-keyring-2") - mockItemHandler := func(item *sdp.Item) { - items = append(items, item) - wg.Done() + item1 := &sdp.Item{ + Type: gcpshared.CloudKMSKeyRing.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs1, + Scope: projectID, } - mockErrorHandler := func(err error) { - errs = append(errs, err) + item2 := &sdp.Item{ + Type: gcpshared.CloudKMSKeyRing.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs2, + Scope: projectID, } - stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) - // Check if adapter supports search streaming - searchStreamable, ok := adapter.(discovery.SearchStreamableAdapter) + listCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_LIST, projectID, gcpshared.CloudKMSKeyRing.String(), "") + cache.StoreItem(ctx, item1, shared.DefaultCacheDuration, listCacheKey) + cache.StoreItem(ctx, item2, 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) + + listable, ok := adapter.(discovery.ListableAdapter) if !ok { - t.Fatalf("Adapter does not support SearchStream operation") + t.Fatalf("Adapter does not support List operation") } - searchStreamable.SearchStream(ctx, wrapper.Scopes()[0], location, true, stream) - wg.Wait() - - if len(errs) > 0 { - t.Fatalf("Expected no errors, got: %v", errs) + items, qErr := listable.List(ctx, wrapper.Scopes()[0], false) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) } + if len(items) != 2 { t.Fatalf("Expected 2 items, got: %d", len(items)) } - for _, item := range items { - if item.Validate() != nil { - t.Fatalf("Expected no validation error, got: %v", item.Validate()) - } - } - - // Verify adapter supports ListStream - _, ok = adapter.(discovery.ListStreamableAdapter) - if !ok { - t.Fatalf("Adapter should support ListStream operation") - } }) - t.Run("List", func(t *testing.T) { - wrapper := manual.NewCloudKMSKeyRing(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - // Mock ListLocations - mockLocationIterator := mocks.NewMockCloudKMSLocationIterator(ctrl) - mockLocationIterator.EXPECT().Next().Return(createLocation(projectID, "us-central1"), nil) - mockLocationIterator.EXPECT().Next().Return(createLocation(projectID, "europe-west1"), nil) - mockLocationIterator.EXPECT().Next().Return(nil, iterator.Done) - - mockClient.EXPECT().ListLocations(ctx, gomock.Any()).Return(mockLocationIterator) - - // Mock Search for first location (us-central1) - mockKeyRingIterator1 := mocks.NewMockCloudKMSKeyRingIterator(ctrl) - mockKeyRingIterator1.EXPECT().Next().Return(createKeyRing(projectID, "us-central1", "keyring-1"), nil) - mockKeyRingIterator1.EXPECT().Next().Return(nil, iterator.Done) - - // Mock Search for second location (europe-west1) - mockKeyRingIterator2 := mocks.NewMockCloudKMSKeyRingIterator(ctrl) - mockKeyRingIterator2.EXPECT().Next().Return(createKeyRing(projectID, "europe-west1", "keyring-2"), nil) - mockKeyRingIterator2.EXPECT().Next().Return(createKeyRing(projectID, "europe-west1", "keyring-3"), nil) - mockKeyRingIterator2.EXPECT().Next().Return(nil, iterator.Done) - - // Expect Search calls for both locations - // Use gomock.Any() for ctx because the pool with context wraps it with cancellation - mockClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *kmspb.ListKeyRingsRequest, opts ...any) gcpshared.CloudKMSKeyRingIterator { - if strings.Contains(req.GetParent(), "us-central1") { - return mockKeyRingIterator1 + t.Run("List_CacheHit_Empty", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Store NOTFOUND error in cache to simulate empty result + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "No resources found in Cloud Asset API", } - return mockKeyRingIterator2 - }).Times(2) + 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) - // Check if adapter supports listing 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) - } - - // Expect 3 items total (1 from us-central1, 2 from europe-west1) - if len(sdpItems) != 3 { - t.Fatalf("Expected 3 items, got: %d", len(sdpItems)) + items, qErr := listable.List(ctx, wrapper.Scopes()[0], false) + if qErr != nil { + t.Fatalf("Expected no error (empty list is valid), got: %v", qErr) } - for _, item := range sdpItems { - if item.Validate() != nil { - t.Fatalf("Expected no validation error, got: %v", item.Validate()) - } + // Empty result is valid for LIST - should return empty slice, not error + if len(items) != 0 { + t.Fatalf("Expected 0 items (empty result), got: %d", len(items)) } }) - t.Run("ListStream", func(t *testing.T) { - wrapper := manual.NewCloudKMSKeyRing(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - - // Mock ListLocations - mockLocationIterator := mocks.NewMockCloudKMSLocationIterator(ctrl) - mockLocationIterator.EXPECT().Next().Return(createLocation(projectID, "us-central1"), nil) - mockLocationIterator.EXPECT().Next().Return(createLocation(projectID, "europe-west1"), nil) - mockLocationIterator.EXPECT().Next().Return(nil, iterator.Done) - - mockClient.EXPECT().ListLocations(ctx, gomock.Any()).Return(mockLocationIterator) - - // Mock Search for first location (us-central1) - mockKeyRingIterator1 := mocks.NewMockCloudKMSKeyRingIterator(ctrl) - mockKeyRingIterator1.EXPECT().Next().Return(createKeyRing(projectID, "us-central1", "keyring-1"), nil) - mockKeyRingIterator1.EXPECT().Next().Return(nil, iterator.Done) - - // Mock Search for second location (europe-west1) - mockKeyRingIterator2 := mocks.NewMockCloudKMSKeyRingIterator(ctrl) - mockKeyRingIterator2.EXPECT().Next().Return(createKeyRing(projectID, "europe-west1", "keyring-2"), nil) - mockKeyRingIterator2.EXPECT().Next().Return(createKeyRing(projectID, "europe-west1", "keyring-3"), nil) - mockKeyRingIterator2.EXPECT().Next().Return(nil, iterator.Done) - - // Expect Search calls for both locations (order may vary due to parallelism) - // Use gomock.Any() for ctx because the pool with context wraps it with cancellation - mockClient.EXPECT().Search(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *kmspb.ListKeyRingsRequest, opts ...any) gcpshared.CloudKMSKeyRingIterator { - if strings.Contains(req.GetParent(), "us-central1") { - return mockKeyRingIterator1 - } - return mockKeyRingIterator2 - }).Times(2) - - var items []*sdp.Item - var itemsMu sync.Mutex - var errs []error - var errsMu sync.Mutex - wg := &sync.WaitGroup{} - wg.Add(3) // 3 total items expected - - mockItemHandler := func(item *sdp.Item) { - itemsMu.Lock() - items = append(items, item) - itemsMu.Unlock() - wg.Done() - } - mockErrorHandler := func(err error) { - errsMu.Lock() - errs = append(errs, err) - errsMu.Unlock() + t.Run("Search_CacheHit", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Pre-populate cache with KeyRing items under SEARCH cache key (by location) + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring", + "uniqueAttr": "us|test-keyring", + }) + _ = attrs.Set("uniqueAttr", "us|test-keyring") + + item := &sdp.Item{ + Type: gcpshared.CloudKMSKeyRing.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: projectID, } - stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSKeyRing.String(), "us") + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, searchCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) - // Check if adapter supports list streaming - listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + wrapper := manual.NewCloudKMSKeyRing(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + + searchable, ok := adapter.(discovery.SearchableAdapter) if !ok { - t.Fatalf("Adapter does not support ListStream operation") + t.Fatalf("Adapter does not support Search operation") } - listStreamable.ListStream(ctx, wrapper.Scopes()[0], true, stream) - wg.Wait() + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], "us", false) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } - if len(errs) > 0 { - t.Fatalf("Expected no errors, got: %v", errs) + if len(items) != 1 { + t.Fatalf("Expected 1 item, got: %d", len(items)) } - if len(items) != 3 { - t.Fatalf("Expected 3 items, got: %d", len(items)) + }) + + t.Run("StaticTests", func(t *testing.T) { + cache := sdpcache.NewCache(ctx) + defer cache.Clear() + + // Pre-populate cache with a KeyRing item + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "projects/test-project-id/locations/us/keyRings/test-keyring", + "uniqueAttr": "us|test-keyring", + }) + _ = attrs.Set("uniqueAttr", "us|test-keyring") + + item := &sdp.Item{ + Type: gcpshared.CloudKMSKeyRing.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: projectID, + LinkedItemQueries: []*sdp.LinkedItemQuery{ + { + Query: &sdp.Query{ + Type: gcpshared.IAMPolicy.String(), + Method: sdp.QueryMethod_GET, + Query: "us|test-keyring", + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + { + Query: &sdp.Query{ + Type: gcpshared.CloudKMSCryptoKey.String(), + Method: sdp.QueryMethod_SEARCH, + Query: "us|test-keyring", + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: false, + Out: true, + }, + }, + }, } - for _, item := range items { - if item.Validate() != nil { - t.Fatalf("Expected no validation error, got: %v", item.Validate()) - } + + cacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_GET, projectID, gcpshared.CloudKMSKeyRing.String(), "us|test-keyring") + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + + 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) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "us|test-keyring", false) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) } - }) -} -// createKeyRing creates a KeyRing with the specified project, location, and keyRing name. -func createKeyRing(projectID, location, keyRingName string) *kmspb.KeyRing { - return &kmspb.KeyRing{ - Name: "projects/" + projectID + "/locations/" + location + "/keyRings/" + keyRingName, - CreateTime: nil, // You can set a timestamp if needed - } -} + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.IAMPolicy.String(), + 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, + }, + }, + } -// createLocation creates a Location with the specified project and location ID. -func createLocation(projectID, locationID string) *locationpb.Location { - return &locationpb.Location{ - Name: "projects/" + projectID + "/locations/" + locationID, - LocationId: locationID, - DisplayName: locationID, - } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) } diff --git a/sources/gcp/manual/compute-instance-group-manager-shared.go b/sources/gcp/manual/compute-instance-group-manager-shared.go new file mode 100644 index 00000000..839f70ef --- /dev/null +++ b/sources/gcp/manual/compute-instance-group-manager-shared.go @@ -0,0 +1,241 @@ +package manual + +import ( + "context" + "strings" + + "cloud.google.com/go/compute/apiv1/computepb" + + "github.com/overmindtech/cli/sdp-go" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/shared" +) + +// InstanceGroupManagerToSDPItem converts a GCP InstanceGroupManager to an SDP Item. +// This function is shared between zonal and regional instance group manager adapters. +// The itemType parameter determines which Overmind type the SDP item will have. +func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *computepb.InstanceGroupManager, location gcpshared.LocationInfo, itemType shared.ItemType) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(instanceGroupManager, "") + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + } + } + + sdpItem := &sdp.Item{ + Type: itemType.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: location.ToScope(), + } + + // Deleting the Instance Group Manager: + // If the IGM is deleted, the associated instances are also deleted, but the instance template remains unaffected. + // The instance template can still be used by other IGMs or for creating standalone instances. + // Deleting an instance template also doesn't not delete the IGM. + + // Link instance template + if instanceTemplate := instanceGroupManager.GetInstanceTemplate(); instanceTemplate != "" { + instanceTemplateName := gcpshared.LastPathComponent(instanceTemplate) + scope, err := gcpshared.ExtractScopeFromURI(ctx, instanceTemplate) + if err == nil && instanceTemplateName != "" { + templateType := gcpshared.ComputeInstanceTemplate + if strings.Contains(instanceTemplate, "/regions/") { + templateType = gcpshared.ComputeRegionInstanceTemplate + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: templateType.String(), + Method: sdp.QueryMethod_GET, + Query: instanceTemplateName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }) + } + } + + // Link instance group + if group := instanceGroupManager.GetInstanceGroup(); group != "" { + instanceGroupName := gcpshared.LastPathComponent(group) + scope, err := gcpshared.ExtractScopeFromURI(ctx, group) + if err == nil && instanceGroupName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeInstanceGroup.String(), + Method: sdp.QueryMethod_GET, + Query: instanceGroupName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + } + + // Link zone (for zonal instance group managers) + if zone := instanceGroupManager.GetZone(); zone != "" { + zoneName := gcpshared.LastPathComponent(zone) + if zoneName != "" && location.ProjectID != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeZone.String(), + Method: sdp.QueryMethod_GET, + Query: zoneName, + Scope: location.ProjectID, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }) + } + } + + // Link region (for regional instance group managers) + if region := instanceGroupManager.GetRegion(); region != "" { + regionName := gcpshared.LastPathComponent(region) + if regionName != "" && location.ProjectID != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeRegion.String(), + Method: sdp.QueryMethod_GET, + Query: regionName, + Scope: location.ProjectID, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }) + } + } + + // Link zones from distribution policy (for regional MIGs with explicit zone distribution) + if distributionPolicy := instanceGroupManager.GetDistributionPolicy(); distributionPolicy != nil { + for _, zoneConfig := range distributionPolicy.GetZones() { + if zoneURL := zoneConfig.GetZone(); zoneURL != "" { + zoneName := gcpshared.LastPathComponent(zoneURL) + if zoneName != "" && location.ProjectID != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeZone.String(), + Method: sdp.QueryMethod_GET, + Query: zoneName, + Scope: location.ProjectID, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }) + } + } + } + } + + // Link target pools + for _, targetPool := range instanceGroupManager.GetTargetPools() { + targetPoolName := gcpshared.LastPathComponent(targetPool) + scope, err := gcpshared.ExtractScopeFromURI(ctx, targetPool) + if err == nil && targetPoolName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeTargetPool.String(), + Method: sdp.QueryMethod_GET, + Query: targetPoolName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + } + + // Link resource policies from ResourcePolicies.WorkloadPolicy + if resourcePolicies := instanceGroupManager.GetResourcePolicies(); resourcePolicies != nil { + if workloadPolicy := resourcePolicies.GetWorkloadPolicy(); workloadPolicy != "" { + resourcePolicyName := gcpshared.LastPathComponent(workloadPolicy) + scope, err := gcpshared.ExtractScopeFromURI(ctx, workloadPolicy) + if err == nil && resourcePolicyName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeResourcePolicy.String(), + Method: sdp.QueryMethod_GET, + Query: resourcePolicyName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }) + } + } + } + + // Link to instance templates in versions array (used for canary/rolling deployments) + // If versions are defined, they override the top-level instanceTemplate + // Each version can have its own template, so we need to link all of them + for _, version := range instanceGroupManager.GetVersions() { + if versionTemplate := version.GetInstanceTemplate(); versionTemplate != "" { + versionTemplateName := gcpshared.LastPathComponent(versionTemplate) + scope, err := gcpshared.ExtractScopeFromURI(ctx, versionTemplate) + if err == nil && versionTemplateName != "" { + templateType := gcpshared.ComputeInstanceTemplate + if strings.Contains(versionTemplate, "/regions/") { + templateType = gcpshared.ComputeRegionInstanceTemplate + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: templateType.String(), + Method: sdp.QueryMethod_GET, + Query: versionTemplateName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }) + } + } + } + + // Link to health checks used in auto-healing policies + // Auto-healing policies use health checks to determine if instances are healthy + // If the health check is deleted or updated, auto-healing may fail + for _, autoHealingPolicy := range instanceGroupManager.GetAutoHealingPolicies() { + if healthCheckURL := autoHealingPolicy.GetHealthCheck(); healthCheckURL != "" { + healthCheckName := gcpshared.LastPathComponent(healthCheckURL) + scope, err := gcpshared.ExtractScopeFromURI(ctx, healthCheckURL) + if err == nil && healthCheckName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeHealthCheck.String(), + Method: sdp.QueryMethod_GET, + Query: healthCheckName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + } + + // Autoscalers set the Instance Group Manager target size + // InstanceGroupManagers orphans the autoscaler when deleted + if status := instanceGroupManager.GetStatus(); status != nil { + if autoscalerURL := status.GetAutoscaler(); autoscalerURL != "" { + autoscalerName := gcpshared.LastPathComponent(autoscalerURL) + scope, err := gcpshared.ExtractScopeFromURI(ctx, autoscalerURL) + if err == nil && autoscalerName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeAutoscaler.String(), + Method: sdp.QueryMethod_GET, + Query: autoscalerName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + } + } + + switch { + case instanceGroupManager.GetStatus() != nil && instanceGroupManager.GetStatus().GetIsStable(): + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + + return sdpItem, nil +} diff --git a/sources/gcp/manual/compute-instance-group-manager.go b/sources/gcp/manual/compute-instance-group-manager.go index 8015886a..15242e86 100644 --- a/sources/gcp/manual/compute-instance-group-manager.go +++ b/sources/gcp/manual/compute-instance-group-manager.go @@ -3,7 +3,6 @@ package manual import ( "context" "errors" - "strings" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -58,6 +57,8 @@ func (c computeInstanceGroupManagerWrapper) PotentialLinks() map[shared.ItemType gcpshared.ComputeResourcePolicy, gcpshared.ComputeAutoscaler, gcpshared.ComputeHealthCheck, + gcpshared.ComputeZone, + gcpshared.ComputeRegion, ) } @@ -218,175 +219,5 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con } func (c computeInstanceGroupManagerWrapper) gcpInstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *computepb.InstanceGroupManager, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { - attributes, err := shared.ToAttributesWithExclude(instanceGroupManager, "") - if err != nil { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: err.Error(), - } - } - - sdpItem := &sdp.Item{ - Type: gcpshared.ComputeInstanceGroupManager.String(), - UniqueAttribute: "name", - Attributes: attributes, - Scope: location.ToScope(), - } - - // Deleting the Instance Group Manager: - // If the IGM is deleted, the associated instances are also deleted, but the instance template remains unaffected. - // The instance template can still be used by other IGMs or for creating standalone instances. - // Deleting an instance template also doesn't not delete the IGM. - - // Link instance template - if instanceTemplate := instanceGroupManager.GetInstanceTemplate(); instanceTemplate != "" { - instanceTemplateName := gcpshared.LastPathComponent(instanceTemplate) - scope, err := gcpshared.ExtractScopeFromURI(ctx, instanceTemplate) - if err == nil && instanceTemplateName != "" { - templateType := gcpshared.ComputeInstanceTemplate - if strings.Contains(instanceTemplate, "/regions/") { - templateType = gcpshared.ComputeRegionInstanceTemplate - } - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: templateType.String(), - Method: sdp.QueryMethod_GET, - Query: instanceTemplateName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, - }) - } - } - - // Link instance group - if group := instanceGroupManager.GetInstanceGroup(); group != "" { - instanceGroupName := gcpshared.LastPathComponent(group) - scope, err := gcpshared.ExtractScopeFromURI(ctx, group) - if err == nil && instanceGroupName != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.ComputeInstanceGroup.String(), - Method: sdp.QueryMethod_GET, - Query: instanceGroupName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, - }) - } - } - - // Link target pools - for _, targetPool := range instanceGroupManager.GetTargetPools() { - targetPoolName := gcpshared.LastPathComponent(targetPool) - scope, err := gcpshared.ExtractScopeFromURI(ctx, targetPool) - if err == nil && targetPoolName != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.ComputeTargetPool.String(), - Method: sdp.QueryMethod_GET, - Query: targetPoolName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, - }) - } - } - - // Link resource policies from ResourcePolicies.WorkloadPolicy - if resourcePolicies := instanceGroupManager.GetResourcePolicies(); resourcePolicies != nil { - if workloadPolicy := resourcePolicies.GetWorkloadPolicy(); workloadPolicy != "" { - resourcePolicyName := gcpshared.LastPathComponent(workloadPolicy) - scope, err := gcpshared.ExtractScopeFromURI(ctx, workloadPolicy) - if err == nil && resourcePolicyName != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.ComputeResourcePolicy.String(), - Method: sdp.QueryMethod_GET, - Query: resourcePolicyName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, - }) - } - } - } - - // Link to instance templates in versions array (used for canary/rolling deployments) - // If versions are defined, they override the top-level instanceTemplate - // Each version can have its own template, so we need to link all of them - for _, version := range instanceGroupManager.GetVersions() { - if versionTemplate := version.GetInstanceTemplate(); versionTemplate != "" { - versionTemplateName := gcpshared.LastPathComponent(versionTemplate) - scope, err := gcpshared.ExtractScopeFromURI(ctx, versionTemplate) - if err == nil && versionTemplateName != "" { - templateType := gcpshared.ComputeInstanceTemplate - if strings.Contains(versionTemplate, "/regions/") { - templateType = gcpshared.ComputeRegionInstanceTemplate - } - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: templateType.String(), - Method: sdp.QueryMethod_GET, - Query: versionTemplateName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, - }) - } - } - } - - // Link to health checks used in auto-healing policies - // Auto-healing policies use health checks to determine if instances are healthy - // If the health check is deleted or updated, auto-healing may fail - for _, autoHealingPolicy := range instanceGroupManager.GetAutoHealingPolicies() { - if healthCheckURL := autoHealingPolicy.GetHealthCheck(); healthCheckURL != "" { - healthCheckName := gcpshared.LastPathComponent(healthCheckURL) - scope, err := gcpshared.ExtractScopeFromURI(ctx, healthCheckURL) - if err == nil && healthCheckName != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.ComputeHealthCheck.String(), - Method: sdp.QueryMethod_GET, - Query: healthCheckName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } - } - } - - // Autoscalers set the Instance Group Manager target size - // InstanceGroupManagers orphans the autoscaler when deleted - if status := instanceGroupManager.GetStatus(); status != nil { - if autoscalerURL := status.GetAutoscaler(); autoscalerURL != "" { - autoscalerName := gcpshared.LastPathComponent(autoscalerURL) - scope, err := gcpshared.ExtractScopeFromURI(ctx, autoscalerURL) - if err == nil && autoscalerName != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.ComputeAutoscaler.String(), - Method: sdp.QueryMethod_GET, - Query: autoscalerName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, - }) - } - } - } - - switch { - case instanceGroupManager.GetStatus() != nil && instanceGroupManager.GetStatus().GetIsStable(): - sdpItem.Health = sdp.Health_HEALTH_OK.Enum() - default: - sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() - } - - return sdpItem, nil + return InstanceGroupManagerToSDPItem(ctx, instanceGroupManager, location, gcpshared.ComputeInstanceGroupManager) } diff --git a/sources/gcp/manual/compute-instance-group-manager_test.go b/sources/gcp/manual/compute-instance-group-manager_test.go index 92228744..913392ef 100644 --- a/sources/gcp/manual/compute-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-instance-group-manager_test.go @@ -82,6 +82,16 @@ func TestComputeInstanceGroupManager(t *testing.T) { 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, @@ -141,6 +151,16 @@ func TestComputeInstanceGroupManager(t *testing.T) { 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, @@ -266,6 +286,7 @@ func TestComputeInstanceGroupManager(t *testing.T) { Status: &computepb.InstanceGroupManagerStatus{ IsStable: ptr.To(true), }, + Zone: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a"), InstanceTemplate: ptr.To(instanceTemplateName), InstanceGroup: ptr.To("projects/test-project-id/zones/us-central1-a/instanceGroups/test-group"), AutoHealingPolicies: []*computepb.InstanceGroupManagerAutoHealingPolicy{ @@ -323,6 +344,16 @@ func TestComputeInstanceGroupManager(t *testing.T) { 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, @@ -485,6 +516,7 @@ func createInstanceGroupManager(name string, isStable bool, instanceTemplate str Status: &computepb.InstanceGroupManagerStatus{ IsStable: ptr.To(isStable), }, + Zone: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a"), InstanceTemplate: ptr.To(instanceTemplate), InstanceGroup: ptr.To("projects/test-project-id/zones/us-central1-a/instanceGroups/test-group"), TargetPools: []string{ diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 481f09f4..46fadc07 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -63,6 +63,9 @@ func (c computeInstanceWrapper) PotentialLinks() map[shared.ItemType]bool { gcpshared.CloudKMSCryptoKey, gcpshared.CloudKMSCryptoKeyVersion, gcpshared.ComputeZone, + gcpshared.ComputeInstanceTemplate, + gcpshared.ComputeRegionInstanceTemplate, + gcpshared.ComputeInstanceGroupManager, ) } @@ -582,6 +585,61 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, } } + // Link to instance template and instance group manager from metadata + if metadata := instance.GetMetadata(); metadata != nil { + for _, item := range metadata.GetItems() { + key := item.GetKey() + value := item.GetValue() + + switch key { + case "instance-template": + // Link to instance template (global or regional) + if value != "" { + templateName := gcpshared.LastPathComponent(value) + scope, err := gcpshared.ExtractScopeFromURI(ctx, value) + if err == nil && templateName != "" { + templateType := gcpshared.ComputeInstanceTemplate + if strings.Contains(value, "/regions/") { + templateType = gcpshared.ComputeRegionInstanceTemplate + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: templateType.String(), + Method: sdp.QueryMethod_GET, + Query: templateName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + case "created-by": + // Link to instance group manager (zonal or regional) + if value != "" { + igmName := gcpshared.LastPathComponent(value) + scope, err := gcpshared.ExtractScopeFromURI(ctx, value) + if err == nil && igmName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeInstanceGroupManager.String(), + Method: sdp.QueryMethod_GET, + Query: igmName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + } + } + } + // Set health based on status switch instance.GetStatus() { case computepb.Instance_RUNNING.String(): diff --git a/sources/gcp/manual/compute-instance_test.go b/sources/gcp/manual/compute-instance_test.go index 674fa129..2b60d307 100644 --- a/sources/gcp/manual/compute-instance_test.go +++ b/sources/gcp/manual/compute-instance_test.go @@ -751,6 +751,240 @@ func TestComputeInstance(t *testing.T) { }) }) + t.Run("GetWithMetadata", func(t *testing.T) { + wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + + // Test with instance-template and created-by metadata + instanceTemplateName := "my-template" + instanceTemplateURI := fmt.Sprintf("projects/%s/global/instanceTemplates/%s", projectID, instanceTemplateName) + igmName := "my-mig" + igmURI := fmt.Sprintf("projects/%s/regions/us-central1/instanceGroupManagers/%s", projectID, igmName) + + instance := createComputeInstance("test-instance", computepb.Instance_RUNNING) + instance.Metadata = &computepb.Metadata{ + Items: []*computepb.Items{ + { + Key: ptr.To("instance-template"), + Value: ptr.To(instanceTemplateURI), + }, + { + Key: ptr.To("created-by"), + Value: ptr.To(igmURI), + }, + }, + } + + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(instance, nil) + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + 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) { + // Base queries that are always present + baseQueries := shared.QueryTests{ + { + ExpectedType: gcpshared.ComputeDisk.String(), + 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, + }, + }, + } + + // Add the metadata-based links + queryTests := append(baseQueries, + shared.QueryTest{ + ExpectedType: gcpshared.ComputeInstanceTemplate.String(), + 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, + }, + }, + ) + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithRegionalInstanceTemplate", func(t *testing.T) { + wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + + // Test with regional instance template + instanceTemplateName := "my-regional-template" + instanceTemplateURI := fmt.Sprintf("projects/%s/regions/us-central1/instanceTemplates/%s", projectID, instanceTemplateName) + + instance := createComputeInstance("test-instance", computepb.Instance_RUNNING) + instance.Metadata = &computepb.Metadata{ + Items: []*computepb.Items{ + { + Key: ptr.To("instance-template"), + Value: ptr.To(instanceTemplateURI), + }, + }, + } + + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(instance, nil) + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + 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) { + // Base queries that are always present + baseQueries := shared.QueryTests{ + { + ExpectedType: gcpshared.ComputeDisk.String(), + 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, + }, + }, + } + + // Add the metadata-based link for regional instance template + queryTests := append(baseQueries, + shared.QueryTest{ + ExpectedType: gcpshared.ComputeRegionInstanceTemplate.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: instanceTemplateName, + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + ) + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + t.Run("SupportsWildcardScope", func(t *testing.T) { wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/compute-region-instance-group-manager.go b/sources/gcp/manual/compute-region-instance-group-manager.go new file mode 100644 index 00000000..ed159890 --- /dev/null +++ b/sources/gcp/manual/compute-region-instance-group-manager.go @@ -0,0 +1,202 @@ +package manual + +import ( + "context" + "errors" + + "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/sources" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeRegionInstanceGroupManagerLookupByName = shared.NewItemTypeLookup("name", gcpshared.ComputeRegionInstanceGroupManager) + +type computeRegionInstanceGroupManagerWrapper struct { + client gcpshared.RegionInstanceGroupManagerClient + *gcpshared.RegionBase +} + +// NewComputeRegionInstanceGroupManager creates a new computeRegionInstanceGroupManagerWrapper. +func NewComputeRegionInstanceGroupManager(client gcpshared.RegionInstanceGroupManagerClient, locations []gcpshared.LocationInfo) sources.ListStreamableWrapper { + return &computeRegionInstanceGroupManagerWrapper{ + client: client, + RegionBase: gcpshared.NewRegionBase( + locations, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + gcpshared.ComputeRegionInstanceGroupManager, + ), + } +} + +func (c computeRegionInstanceGroupManagerWrapper) IAMPermissions() []string { + return []string{ + "compute.regionInstanceGroupManagers.get", + "compute.regionInstanceGroupManagers.list", + } +} + +func (c computeRegionInstanceGroupManagerWrapper) PredefinedRole() string { + return "roles/compute.viewer" +} + +// PotentialLinks returns the potential links for the regional compute instance group manager wrapper +func (c computeRegionInstanceGroupManagerWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + gcpshared.ComputeInstanceTemplate, + gcpshared.ComputeRegionInstanceTemplate, + gcpshared.ComputeInstanceGroup, + gcpshared.ComputeTargetPool, + gcpshared.ComputeResourcePolicy, + gcpshared.ComputeAutoscaler, + gcpshared.ComputeHealthCheck, + gcpshared.ComputeZone, + gcpshared.ComputeRegion, + ) +} + +// TerraformMappings returns the Terraform mappings for the regional compute instance group manager wrapper +func (c computeRegionInstanceGroupManagerWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + // https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_region_instance_group_manager#argument-reference + TerraformQueryMap: "google_compute_region_instance_group_manager.name", + }, + } +} + +// GetLookups returns the lookups for the regional compute instance group manager wrapper +func (c computeRegionInstanceGroupManagerWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeRegionInstanceGroupManagerLookupByName, + } +} + +// SupportsWildcardScope implements the WildcardScopeAdapter interface +// Returns true for regional compute instance group managers since they can list across all regions +func (c computeRegionInstanceGroupManagerWrapper) SupportsWildcardScope() bool { + return true +} + +// Get retrieves a regional compute instance group manager by its name +func (c computeRegionInstanceGroupManagerWrapper) 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(), + } + } + + req := &computepb.GetRegionInstanceGroupManagerRequest{ + Project: location.ProjectID, + Region: location.Region, + InstanceGroupManager: queryParts[0], + } + + igm, getErr := c.client.Get(ctx, req) + if getErr != nil { + return nil, gcpshared.QueryError(getErr, scope, c.Type()) + } + + return c.gcpRegionInstanceGroupManagerToSDPItem(ctx, igm, location) +} + +func (c computeRegionInstanceGroupManagerWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + return gcpshared.CollectFromStream(ctx, func(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey) { + c.ListStream(ctx, stream, cache, cacheKey, scope) + }) +} + +func (c computeRegionInstanceGroupManagerWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + // Handle wildcard scope by listing across all configured regions + if scope == "*" { + c.listAllRegionsStream(ctx, stream, cache, cacheKey) + return + } + + // Handle specific regional scope with per-region List + location, err := c.LocationFromScope(scope) + if err != nil { + stream.SendError(&sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + }) + return + } + + it := c.client.List(ctx, &computepb.ListRegionInstanceGroupManagersRequest{ + Project: location.ProjectID, + Region: location.Region, + }) + + for { + igm, iterErr := it.Next() + if errors.Is(iterErr, iterator.Done) { + break + } + if iterErr != nil { + stream.SendError(gcpshared.QueryError(iterErr, scope, c.Type())) + return + } + + item, sdpErr := c.gcpRegionInstanceGroupManagerToSDPItem(ctx, igm, location) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } +} + +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) + + for _, location := range c.Locations() { + p.Go(func(ctx context.Context) error { + it := c.client.List(ctx, &computepb.ListRegionInstanceGroupManagersRequest{ + Project: location.ProjectID, + Region: location.Region, + }) + + for { + igm, iterErr := it.Next() + if errors.Is(iterErr, iterator.Done) { + break + } + if iterErr != nil { + stream.SendError(gcpshared.QueryError(iterErr, location.ToScope(), c.Type())) + return iterErr + } + + item, sdpErr := c.gcpRegionInstanceGroupManagerToSDPItem(ctx, igm, location) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + + return nil + }) + } + + // Wait for all goroutines to complete + _ = p.Wait() +} + +func (c computeRegionInstanceGroupManagerWrapper) gcpRegionInstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *computepb.InstanceGroupManager, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { + return InstanceGroupManagerToSDPItem(ctx, instanceGroupManager, location, gcpshared.ComputeRegionInstanceGroupManager) +} diff --git a/sources/gcp/manual/compute-region-instance-group-manager_test.go b/sources/gcp/manual/compute-region-instance-group-manager_test.go new file mode 100644 index 00000000..582751cd --- /dev/null +++ b/sources/gcp/manual/compute-region-instance-group-manager_test.go @@ -0,0 +1,307 @@ +package manual_test + +import ( + "context" + "fmt" + "testing" + + "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/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" +) + +func TestComputeRegionInstanceGroupManager(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mocks.NewMockRegionInstanceGroupManagerClient(ctrl) + projectID := "test-project-id" + region := "us-central1" + instanceTemplateName := "https://www.googleapis.com/compute/v1/projects/test-project-id/global/instanceTemplates/unit-test-template" + + t.Run("Get", func(t *testing.T) { + wrapper := manual.NewComputeRegionInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createRegionInstanceGroupManager("test-region-instance-group-manager", true, instanceTemplateName), nil) + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-region-instance-group-manager", true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != gcpshared.ComputeRegionInstanceGroupManager.String() { + t.Fatalf("Expected type %s, got: %s", gcpshared.ComputeRegionInstanceGroupManager.String(), sdpItem.GetType()) + } + + t.Run("StaticTests", func(t *testing.T) { + t.Run("GlobalInstanceTemplate", func(t *testing.T) { + igm := createRegionInstanceGroupManager("test-region-instance-group-manager", true, instanceTemplateName) + + wrapper := manual.NewComputeRegionInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(igm, nil) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-region-instance-group-manager", true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.ComputeInstanceTemplate.String(), + 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) + }) + + t.Run("RegionalInstanceTemplate", func(t *testing.T) { + regionalInstanceTemplateName := "https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/instanceTemplates/regional-template" + igm := createRegionInstanceGroupManager("test-region-instance-group-manager", true, regionalInstanceTemplateName) + + wrapper := manual.NewComputeRegionInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(igm, nil) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-region-instance-group-manager", true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.ComputeRegionInstanceTemplate.String(), + 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) + }) + }) + }) + + t.Run("HealthCheck", func(t *testing.T) { + healthTests := []struct { + name string + isStable bool + expectedHealth sdp.Health + }{ + { + name: "Stable", + isStable: true, + expectedHealth: sdp.Health_HEALTH_OK, + }, + { + name: "Unstable", + isStable: false, + expectedHealth: sdp.Health_HEALTH_UNKNOWN, + }, + } + + for _, tc := range healthTests { + t.Run(tc.name, func(t *testing.T) { + wrapper := manual.NewComputeRegionInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createRegionInstanceGroupManager("test-region-instance-group-manager", tc.isStable, instanceTemplateName), nil) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-region-instance-group-manager", true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetHealth() != tc.expectedHealth { + t.Fatalf("Expected health %v, got: %v", tc.expectedHealth, sdpItem.GetHealth()) + } + }) + } + }) + + t.Run("List", func(t *testing.T) { + wrapper := manual.NewComputeRegionInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + + mockIterator := mocks.NewMockRegionInstanceGroupManagerIterator(ctrl) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIterator) + mockIterator.EXPECT().Next().Return(createRegionInstanceGroupManager("region-instance-group-manager-1", true, instanceTemplateName), nil) + mockIterator.EXPECT().Next().Return(createRegionInstanceGroupManager("region-instance-group-manager-2", false, instanceTemplateName), nil) + mockIterator.EXPECT().Next().Return(nil, iterator.Done) + + items, qErr := wrapper.(sources.ListableWrapper).List(ctx, wrapper.Scopes()[0]) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + + for i, item := range items { + expectedName := "region-instance-group-manager-" + fmt.Sprintf("%d", i+1) + if item.GetAttributes().GetAttrStruct().GetFields()["name"].GetStringValue() != expectedName { + t.Fatalf("Expected name %s, got: %s", expectedName, item.GetAttributes().GetAttrStruct().GetFields()["name"].GetStringValue()) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + wrapper := manual.NewComputeRegionInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + + mockIterator := mocks.NewMockRegionInstanceGroupManagerIterator(ctrl) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIterator) + mockIterator.EXPECT().Next().Return(createRegionInstanceGroupManager("region-instance-group-manager-1", true, instanceTemplateName), nil) + mockIterator.EXPECT().Next().Return(createRegionInstanceGroupManager("region-instance-group-manager-2", false, instanceTemplateName), nil) + mockIterator.EXPECT().Next().Return(nil, iterator.Done) + + stream := discovery.NewRecordingQueryResultStream() + noOpCache := sdpcache.NewNoOpCache() + emptyCacheKey := sdpcache.CacheKey{} + + wrapper.ListStream(ctx, stream, noOpCache, emptyCacheKey, wrapper.Scopes()[0]) + + items := stream.GetItems() + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) +} + +func createRegionInstanceGroupManager(name string, isStable bool, instanceTemplate string) *computepb.InstanceGroupManager { + return &computepb.InstanceGroupManager{ + Name: ptr.To(name), + Status: &computepb.InstanceGroupManagerStatus{ + IsStable: ptr.To(isStable), + Autoscaler: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/autoscalers/test-autoscaler"), + }, + Region: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1"), + InstanceTemplate: ptr.To(instanceTemplate), + InstanceGroup: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/instanceGroups/test-group"), + TargetPools: []string{"https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/targetPools/test-pool"}, + ResourcePolicies: &computepb.InstanceGroupManagerResourcePolicies{ + WorkloadPolicy: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), + }, + } +} diff --git a/sources/gcp/shared/compute-clients.go b/sources/gcp/shared/compute-clients.go index 62643051..093047c2 100644 --- a/sources/gcp/shared/compute-clients.go +++ b/sources/gcp/shared/compute-clients.go @@ -176,6 +176,38 @@ func (c computeInstanceGroupManagerClient) AggregatedList(ctx context.Context, r return c.instanceGroupManagersClient.AggregatedList(ctx, req, opts...) } +// RegionInstanceGroupManagerIterator is an interface for iterating over regional instance group managers +type RegionInstanceGroupManagerIterator interface { + Next() (*computepb.InstanceGroupManager, error) +} + +// RegionInstanceGroupManagerClient is an interface for the Compute Region Instance Group Manager client +type RegionInstanceGroupManagerClient interface { + Get(ctx context.Context, req *computepb.GetRegionInstanceGroupManagerRequest, opts ...gax.CallOption) (*computepb.InstanceGroupManager, error) + List(ctx context.Context, req *computepb.ListRegionInstanceGroupManagersRequest, opts ...gax.CallOption) RegionInstanceGroupManagerIterator +} + +type regionInstanceGroupManagerClient struct { + regionInstanceGroupManagersClient *compute.RegionInstanceGroupManagersClient +} + +// NewRegionInstanceGroupManagerClient creates a new RegionInstanceGroupManagerClient +func NewRegionInstanceGroupManagerClient(regionInstanceGroupManagersClient *compute.RegionInstanceGroupManagersClient) RegionInstanceGroupManagerClient { + return ®ionInstanceGroupManagerClient{ + regionInstanceGroupManagersClient: regionInstanceGroupManagersClient, + } +} + +// Get retrieves a regional compute instance group manager +func (c regionInstanceGroupManagerClient) Get(ctx context.Context, req *computepb.GetRegionInstanceGroupManagerRequest, opts ...gax.CallOption) (*computepb.InstanceGroupManager, error) { + return c.regionInstanceGroupManagersClient.Get(ctx, req, opts...) +} + +// List lists regional compute instance group managers and returns an iterator +func (c regionInstanceGroupManagerClient) List(ctx context.Context, req *computepb.ListRegionInstanceGroupManagersRequest, opts ...gax.CallOption) RegionInstanceGroupManagerIterator { + return c.regionInstanceGroupManagersClient.List(ctx, req, opts...) +} + type ForwardingRuleIterator interface { Next() (*computepb.ForwardingRule, error) } diff --git a/sources/gcp/shared/item-types.go b/sources/gcp/shared/item-types.go index a3f01c5d..434637b0 100644 --- a/sources/gcp/shared/item-types.go +++ b/sources/gcp/shared/item-types.go @@ -7,6 +7,7 @@ var ( ComputeInstanceTemplate = shared.NewItemType(GCP, Compute, InstanceTemplate) ComputeMachineImage = shared.NewItemType(GCP, Compute, MachineImage) ComputeInstanceGroupManager = shared.NewItemType(GCP, Compute, InstanceGroupManager) + ComputeRegionInstanceGroupManager = shared.NewItemType(GCP, Compute, RegionalInstanceGroupManager) ComputeSubnetwork = shared.NewItemType(GCP, Compute, Subnetwork) ComputeNetwork = shared.NewItemType(GCP, Compute, Network) ComputeImage = shared.NewItemType(GCP, Compute, Image) diff --git a/sources/gcp/shared/kms-asset-loader.go b/sources/gcp/shared/kms-asset-loader.go new file mode 100644 index 00000000..af9c99b4 --- /dev/null +++ b/sources/gcp/shared/kms-asset-loader.go @@ -0,0 +1,847 @@ +package shared + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "sync" + "time" + + "cloud.google.com/go/kms/apiv1/kmspb" + log "github.com/sirupsen/logrus" + "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/sources/shared" +) + +// CloudKMSAssetLoader handles bulk loading of KMS resources via Cloud Asset API. +// It fetches all KMS resources (KeyRings, CryptoKeys, CryptoKeyVersions) in a single +// API call and stores them in sdpcache for efficient retrieval by adapters. +type CloudKMSAssetLoader struct { + httpClient *http.Client + projectID string + cache sdpcache.Cache + sourceName string + + // TTL-aware reloading + mu sync.Mutex + lastLoadTime time.Time + group singleflight.Group +} + +// NewCloudKMSAssetLoader creates a new CloudKMSAssetLoader. +func NewCloudKMSAssetLoader( + httpClient *http.Client, + projectID string, + cache sdpcache.Cache, + sourceName string, + locations []LocationInfo, +) *CloudKMSAssetLoader { + return &CloudKMSAssetLoader{ + httpClient: httpClient, + projectID: projectID, + cache: cache, + sourceName: sourceName, + } +} + +// EnsureLoaded triggers bulk load if cache TTL has expired. +// Called by adapters on cache miss. +func (l *CloudKMSAssetLoader) EnsureLoaded(ctx context.Context) error { + l.mu.Lock() + timeSinceLastLoad := time.Since(l.lastLoadTime) + l.mu.Unlock() + + // If data was loaded recently, skip reload + if timeSinceLastLoad < shared.DefaultCacheDuration { + return nil + } + + // Use singleflight to ensure only one load runs at a time + // Concurrent callers wait for the same result + _, err, _ := l.group.Do("load", func() (interface{}, error) { + // Double-check TTL after acquiring the flight + l.mu.Lock() + if time.Since(l.lastLoadTime) < shared.DefaultCacheDuration { + l.mu.Unlock() + return nil, nil + } + l.mu.Unlock() + + // Perform the bulk load + if err := l.loadAll(ctx); err != nil { + return nil, err + } + + // Update last load time on success + l.mu.Lock() + l.lastLoadTime = time.Now() + l.mu.Unlock() + + return nil, nil + }) + return err +} + +// cloudAssetResponse represents the response from Cloud Asset API +type cloudAssetResponse struct { + Assets []cloudAsset `json:"assets"` + NextPageToken string `json:"nextPageToken"` +} + +// cloudAsset represents a single asset from Cloud Asset API +type cloudAsset struct { + Name string `json:"name"` + AssetType string `json:"assetType"` + Resource cloudResource `json:"resource"` + Ancestors []string `json:"ancestors"` + UpdateTime string `json:"updateTime"` +} + +// cloudResource contains the actual resource data +type cloudResource struct { + Version string `json:"version"` + DiscoveryDocumentURI string `json:"discoveryDocumentUri"` + DiscoveryName string `json:"discoveryName"` + Parent string `json:"parent"` + Data json.RawMessage `json:"data"` +} + +// loadAll fetches all KMS resources from Cloud Asset API and stores in sdpcache +func (l *CloudKMSAssetLoader) loadAll(ctx context.Context) error { + // Fetch all KMS assets + assets, err := l.fetchAllAssets(ctx) + if err != nil { + return fmt.Errorf("failed to fetch KMS assets: %w", err) + } + + // Track which resource types had items + hasKeyRings := false + hasCryptoKeys := false + hasKeyVersions := false + + // Process and cache each asset + for _, asset := range assets { + switch asset.AssetType { + case "cloudkms.googleapis.com/KeyRing": + hasKeyRings = true + if err := l.cacheKeyRing(ctx, asset); err != nil { + // Log error but continue processing other assets + log.WithContext(ctx).WithError(err).WithFields(log.Fields{ + "ovm.kms.assetType": asset.AssetType, + "ovm.kms.assetName": asset.Name, + }).Warn("failed to cache KMS KeyRing") + continue + } + case "cloudkms.googleapis.com/CryptoKey": + hasCryptoKeys = true + if err := l.cacheCryptoKey(ctx, asset); err != nil { + log.WithContext(ctx).WithError(err).WithFields(log.Fields{ + "ovm.kms.assetType": asset.AssetType, + "ovm.kms.assetName": asset.Name, + }).Warn("failed to cache KMS CryptoKey") + continue + } + case "cloudkms.googleapis.com/CryptoKeyVersion": + hasKeyVersions = true + if err := l.cacheCryptoKeyVersion(ctx, asset); err != nil { + log.WithContext(ctx).WithError(err).WithFields(log.Fields{ + "ovm.kms.assetType": asset.AssetType, + "ovm.kms.assetName": asset.Name, + }).Warn("failed to cache KMS CryptoKeyVersion") + continue + } + } + } + + // For types with no items, store NOTFOUND error so cache.Lookup() returns cacheHit=true + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "No resources found in Cloud Asset API", + } + + scope := l.projectID + + if !hasKeyRings { + listCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_LIST, scope, CloudKMSKeyRing.String(), "") + l.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, listCacheKey) + } + if !hasCryptoKeys { + listCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_LIST, scope, CloudKMSCryptoKey.String(), "") + l.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, listCacheKey) + } + if !hasKeyVersions { + listCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_LIST, scope, CloudKMSCryptoKeyVersion.String(), "") + l.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, listCacheKey) + } + + return nil +} + +// fetchAllAssets fetches all KMS assets from Cloud Asset API with pagination +func (l *CloudKMSAssetLoader) fetchAllAssets(ctx context.Context) ([]cloudAsset, error) { + var allAssets []cloudAsset + pageToken := "" + + for { + assets, nextToken, err := l.fetchAssetsPage(ctx, pageToken) + if err != nil { + return nil, err + } + + allAssets = append(allAssets, assets...) + + if nextToken == "" { + break + } + pageToken = nextToken + } + + return allAssets, nil +} + +// fetchAssetsPage fetches a single page of KMS assets +func (l *CloudKMSAssetLoader) fetchAssetsPage(ctx context.Context, pageToken string) ([]cloudAsset, string, error) { + // Build the Cloud Asset API URL + baseURL := fmt.Sprintf("https://cloudasset.googleapis.com/v1/projects/%s/assets", l.projectID) + + params := url.Values{} + params.Add("assetTypes", "cloudkms.googleapis.com/KeyRing") + params.Add("assetTypes", "cloudkms.googleapis.com/CryptoKey") + params.Add("assetTypes", "cloudkms.googleapis.com/CryptoKeyVersion") + params.Set("contentType", "RESOURCE") + if pageToken != "" { + params.Set("pageToken", pageToken) + } + + apiURL := fmt.Sprintf("%s?%s", baseURL, params.Encode()) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) + if err != nil { + return nil, "", fmt.Errorf("failed to create request: %w", err) + } + + // Cloud Asset API requires quota project header + req.Header.Set("X-Goog-User-Project", l.projectID) + + resp, err := l.httpClient.Do(req) + if err != nil { + return nil, "", fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, "", fmt.Errorf("Cloud Asset API returned status %d: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, "", fmt.Errorf("failed to read response body: %w", err) + } + + var response cloudAssetResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, "", fmt.Errorf("failed to unmarshal response: %w", err) + } + + return response.Assets, response.NextPageToken, nil +} + +// cacheKeyRing converts a Cloud Asset to SDP Item and stores in cache +func (l *CloudKMSAssetLoader) cacheKeyRing(ctx context.Context, asset cloudAsset) error { + // Parse the resource data into KeyRing protobuf + var keyRing kmspb.KeyRing + if err := protojson.Unmarshal(asset.Resource.Data, &keyRing); err != nil { + return fmt.Errorf("failed to unmarshal KeyRing: %w", err) + } + + // Extract path parameters from the asset name + // Format: //cloudkms.googleapis.com/projects/{project}/locations/{location}/keyRings/{keyRing} + resourceName := extractResourceName(asset.Name) + keyRingVals := ExtractPathParams(resourceName, "locations", "keyRings") + if len(keyRingVals) != 2 || keyRingVals[0] == "" || keyRingVals[1] == "" { + return fmt.Errorf("invalid KeyRing name: %s", asset.Name) + } + + // Create unique attribute key (location|keyRingName) + uniqueAttr := shared.CompositeLookupKey(keyRingVals...) + + // Convert to SDP Item + attributes, err := shared.ToAttributesWithExclude(&keyRing) + if err != nil { + return fmt.Errorf("failed to convert KeyRing to attributes: %w", err) + } + + if err := attributes.Set("uniqueAttr", uniqueAttr); err != nil { + return fmt.Errorf("failed to set unique attribute: %w", err) + } + + scope := l.projectID + item := &sdp.Item{ + Type: CloudKMSKeyRing.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Add linked item queries + item.LinkedItemQueries = l.keyRingLinkedQueries(keyRingVals, scope) + + // Store in cache with GET cache key pattern (for individual lookups) + getCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_GET, scope, CloudKMSKeyRing.String(), uniqueAttr) + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, getCacheKey) + + // Also store with LIST cache key (for listing all KeyRings) + listCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_LIST, scope, CloudKMSKeyRing.String(), "") + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, listCacheKey) + + // Also store with SEARCH cache key (for searching by location) + // KeyRing search is by location only + location := keyRingVals[0] + searchCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_SEARCH, scope, CloudKMSKeyRing.String(), location) + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, searchCacheKey) + + return nil +} + +// cacheCryptoKey converts a Cloud Asset to SDP Item and stores in cache +func (l *CloudKMSAssetLoader) cacheCryptoKey(ctx context.Context, asset cloudAsset) error { + // Parse the resource data into CryptoKey protobuf + var cryptoKey kmspb.CryptoKey + if err := protojson.Unmarshal(asset.Resource.Data, &cryptoKey); err != nil { + return fmt.Errorf("failed to unmarshal CryptoKey: %w", err) + } + + // Extract path parameters + // Format: //cloudkms.googleapis.com/projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{cryptoKey} + resourceName := extractResourceName(asset.Name) + values := ExtractPathParams(resourceName, "locations", "keyRings", "cryptoKeys") + if len(values) != 3 || values[0] == "" || values[1] == "" || values[2] == "" { + return fmt.Errorf("invalid CryptoKey name: %s", asset.Name) + } + + // Create unique attribute key (location|keyRing|cryptoKey) + uniqueAttr := shared.CompositeLookupKey(values...) + + // Convert to SDP Item + attributes, err := shared.ToAttributesWithExclude(&cryptoKey, "labels") + if err != nil { + return fmt.Errorf("failed to convert CryptoKey to attributes: %w", err) + } + + if err := attributes.Set("uniqueAttr", uniqueAttr); err != nil { + return fmt.Errorf("failed to set unique attribute: %w", err) + } + + scope := l.projectID + item := &sdp.Item{ + Type: CloudKMSCryptoKey.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: cryptoKey.GetLabels(), + } + + // Add linked item queries + item.LinkedItemQueries = l.cryptoKeyLinkedQueries(values, &cryptoKey, scope) + + // Store in cache with GET cache key (for individual lookups) + getCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_GET, scope, CloudKMSCryptoKey.String(), uniqueAttr) + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, getCacheKey) + + // Also store with LIST cache key (for listing all CryptoKeys) + listCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_LIST, scope, CloudKMSCryptoKey.String(), "") + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, listCacheKey) + + // Also store with SEARCH cache key (for searching by keyRing) + // CryptoKey search is by location|keyRing + location := values[0] + keyRing := values[1] + searchQuery := shared.CompositeLookupKey(location, keyRing) + searchCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_SEARCH, scope, CloudKMSCryptoKey.String(), searchQuery) + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, searchCacheKey) + + return nil +} + +// cacheCryptoKeyVersion converts a Cloud Asset to SDP Item and stores in cache +func (l *CloudKMSAssetLoader) cacheCryptoKeyVersion(ctx context.Context, asset cloudAsset) error { + // Parse the resource data into CryptoKeyVersion protobuf + var keyVersion kmspb.CryptoKeyVersion + if err := protojson.Unmarshal(asset.Resource.Data, &keyVersion); err != nil { + return fmt.Errorf("failed to unmarshal CryptoKeyVersion: %w", err) + } + + // Extract path parameters + // Format: //cloudkms.googleapis.com/projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{cryptoKey}/cryptoKeyVersions/{version} + resourceName := extractResourceName(asset.Name) + values := ExtractPathParams(resourceName, "locations", "keyRings", "cryptoKeys", "cryptoKeyVersions") + if len(values) != 4 || values[0] == "" || values[1] == "" || values[2] == "" || values[3] == "" { + return fmt.Errorf("invalid CryptoKeyVersion name: %s", asset.Name) + } + + // Create unique attribute key (location|keyRing|cryptoKey|version) + uniqueAttr := shared.CompositeLookupKey(values...) + + // Convert to SDP Item + attributes, err := shared.ToAttributesWithExclude(&keyVersion) + if err != nil { + return fmt.Errorf("failed to convert CryptoKeyVersion to attributes: %w", err) + } + + if err := attributes.Set("uniqueAttr", uniqueAttr); err != nil { + return fmt.Errorf("failed to set unique attribute: %w", err) + } + + scope := l.projectID + item := &sdp.Item{ + Type: CloudKMSCryptoKeyVersion.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Add linked item queries + item.LinkedItemQueries = l.cryptoKeyVersionLinkedQueries(values, &keyVersion, scope) + + // Set health based on state + item.Health = l.cryptoKeyVersionHealth(&keyVersion) + + // Store in cache with GET cache key (for individual lookups) + getCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_GET, scope, CloudKMSCryptoKeyVersion.String(), uniqueAttr) + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, getCacheKey) + + // Also store with LIST cache key (for listing all CryptoKeyVersions) + listCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_LIST, scope, CloudKMSCryptoKeyVersion.String(), "") + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, listCacheKey) + + // Also store with SEARCH cache key (for searching by cryptoKey) + // CryptoKeyVersion search is by location|keyRing|cryptoKey + location := values[0] + keyRing := values[1] + cryptoKeyName := values[2] + searchQuery := shared.CompositeLookupKey(location, keyRing, cryptoKeyName) + searchCacheKey := sdpcache.CacheKeyFromParts(l.sourceName, sdp.QueryMethod_SEARCH, scope, CloudKMSCryptoKeyVersion.String(), searchQuery) + l.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, searchCacheKey) + + return nil +} + +// extractResourceName extracts the resource name from Cloud Asset name +// Example: //cloudkms.googleapis.com/projects/my-project/locations/global/keyRings/my-keyring +// Returns: projects/my-project/locations/global/keyRings/my-keyring +func extractResourceName(assetName string) string { + // Remove the //cloudkms.googleapis.com/ prefix + if len(assetName) > 2 && assetName[:2] == "//" { + // Find the first / after the domain + for i := 2; i < len(assetName); i++ { + if assetName[i] == '/' { + return assetName[i+1:] + } + } + } + return assetName +} + +// keyRingLinkedQueries returns linked item queries for a KeyRing +func (l *CloudKMSAssetLoader) keyRingLinkedQueries(keyRingVals []string, scope string) []*sdp.LinkedItemQuery { + var queries []*sdp.LinkedItemQuery + + // Link to IAM Policy + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: IAMPolicy.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(keyRingVals...), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + + // Link to CryptoKeys in this KeyRing + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSCryptoKey.String(), + Method: sdp.QueryMethod_SEARCH, + Query: shared.CompositeLookupKey(keyRingVals[0], keyRingVals[1]), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: false, + Out: true, + }, + }) + + return queries +} + +// cryptoKeyLinkedQueries returns linked item queries for a CryptoKey +func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey *kmspb.CryptoKey, scope string) []*sdp.LinkedItemQuery { + var queries []*sdp.LinkedItemQuery + kmsLocation := values[0] + keyRing := values[1] + cryptoKeyName := values[2] + + // Link to IAM Policy + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: IAMPolicy.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(kmsLocation, keyRing, cryptoKeyName), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + + // Link to parent KeyRing + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSKeyRing.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(kmsLocation, keyRing), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + + // Link to all CryptoKeyVersions + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSCryptoKeyVersion.String(), + Method: sdp.QueryMethod_SEARCH, + Query: shared.CompositeLookupKey(kmsLocation, keyRing, cryptoKeyName), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + + // Link to primary CryptoKeyVersion if present + if primary := cryptoKey.GetPrimary(); primary != nil { + if name := primary.GetName(); name != "" { + keyVersionVals := ExtractPathParams(name, "locations", "keyRings", "cryptoKeys", "cryptoKeyVersions") + if len(keyVersionVals) == 4 && keyVersionVals[0] != "" && keyVersionVals[1] != "" && keyVersionVals[2] != "" && keyVersionVals[3] != "" { + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSCryptoKeyVersion.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(keyVersionVals...), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + } + + // Link to ImportJob if present + if importJob := primary.GetImportJob(); importJob != "" { + importJobVals := ExtractPathParams(importJob, "locations", "keyRings", "importJobs") + if len(importJobVals) == 3 && importJobVals[0] != "" && importJobVals[1] != "" && importJobVals[2] != "" { + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSImportJob.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(importJobVals...), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + + // Link to EKM Connection if applicable + if protectionLevel := primary.GetProtectionLevel(); protectionLevel == kmspb.ProtectionLevel_EXTERNAL_VPC { + if cryptoKeyBackend := cryptoKey.GetCryptoKeyBackend(); cryptoKeyBackend != "" { + backendVals := ExtractPathParams(cryptoKeyBackend, "locations", "ekmConnections") + if len(backendVals) == 2 && backendVals[0] != "" && backendVals[1] != "" { + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSEKMConnection.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(backendVals...), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + } + } + + return queries +} + +// cryptoKeyVersionLinkedQueries returns linked item queries for a CryptoKeyVersion +func (l *CloudKMSAssetLoader) cryptoKeyVersionLinkedQueries(values []string, keyVersion *kmspb.CryptoKeyVersion, scope string) []*sdp.LinkedItemQuery { + var queries []*sdp.LinkedItemQuery + + // Link to parent CryptoKey + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSCryptoKey.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(values[0], values[1], values[2]), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + + // Link to ImportJob if present + if importJob := keyVersion.GetImportJob(); importJob != "" { + importJobVals := ExtractPathParams(importJob, "locations", "keyRings", "importJobs") + if len(importJobVals) == 3 && importJobVals[0] != "" && importJobVals[1] != "" && importJobVals[2] != "" { + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSImportJob.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(importJobVals...), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + + // Link to EKM Connection if applicable + if protectionLevel := keyVersion.GetProtectionLevel(); protectionLevel == kmspb.ProtectionLevel_EXTERNAL_VPC { + if externalProtection := keyVersion.GetExternalProtectionLevelOptions(); externalProtection != nil { + if ekmPath := externalProtection.GetEkmConnectionKeyPath(); ekmPath != "" { + ekmVals := ExtractPathParams(ekmPath, "locations", "ekmConnections") + if len(ekmVals) == 2 && ekmVals[0] != "" && ekmVals[1] != "" { + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: CloudKMSEKMConnection.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(ekmVals...), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + } + } + + return queries +} + +// cryptoKeyVersionHealth returns the health status based on CryptoKeyVersion state +func (l *CloudKMSAssetLoader) cryptoKeyVersionHealth(keyVersion *kmspb.CryptoKeyVersion) *sdp.Health { + switch keyVersion.GetState() { + case kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_STATE_UNSPECIFIED: + return sdp.Health_HEALTH_UNKNOWN.Enum() + case kmspb.CryptoKeyVersion_PENDING_GENERATION, kmspb.CryptoKeyVersion_PENDING_IMPORT: + return sdp.Health_HEALTH_PENDING.Enum() + case kmspb.CryptoKeyVersion_ENABLED: + return sdp.Health_HEALTH_OK.Enum() + case kmspb.CryptoKeyVersion_DISABLED: + return sdp.Health_HEALTH_WARNING.Enum() + case kmspb.CryptoKeyVersion_DESTROYED, kmspb.CryptoKeyVersion_DESTROY_SCHEDULED: + return sdp.Health_HEALTH_ERROR.Enum() + case kmspb.CryptoKeyVersion_IMPORT_FAILED, kmspb.CryptoKeyVersion_GENERATION_FAILED, kmspb.CryptoKeyVersion_EXTERNAL_DESTRUCTION_FAILED: + return sdp.Health_HEALTH_ERROR.Enum() + case kmspb.CryptoKeyVersion_PENDING_EXTERNAL_DESTRUCTION: + return sdp.Health_HEALTH_PENDING.Enum() + default: + return sdp.Health_HEALTH_UNKNOWN.Enum() + } +} + +// GetItem performs the cache-lookup-load-recheck pattern for GET queries. +// Returns the item from cache, loading data if needed. +func (l *CloudKMSAssetLoader) GetItem(ctx context.Context, scope, itemType, uniqueAttr string) (*sdp.Item, *sdp.QueryError) { + // Check cache first + cacheHit, _, cachedItems, cachedErr, done := l.cache.Lookup(ctx, l.sourceName, sdp.QueryMethod_GET, scope, itemType, uniqueAttr, false) + + if cacheHit { + done() + if cachedErr != nil { + return nil, cachedErr + } + if len(cachedItems) > 0 { + return cachedItems[0], nil + } + } + + // Cache miss - trigger lazy bulk load + if err := l.EnsureLoaded(ctx); err != nil { + done() + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: fmt.Sprintf("failed to load KMS data from Cloud Asset API: %v", err), + } + } + + // Complete first lookup's pending work before second lookup to avoid self-deadlock + done() + + // Re-check cache after bulk load + cacheHit, _, cachedItems, cachedErr, done = l.cache.Lookup(ctx, l.sourceName, sdp.QueryMethod_GET, scope, itemType, uniqueAttr, false) + defer done() + + if cacheHit { + if cachedErr != nil { + return nil, cachedErr + } + if len(cachedItems) > 0 { + return cachedItems[0], nil + } + } + + // Item not found (may be newly created, Cloud Asset API has indexing delay) + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("%s %s not found (Cloud Asset API may have indexing delay for new resources)", itemType, uniqueAttr), + } +} + +// SearchItems performs the cache-lookup-load-recheck pattern for SEARCH queries. +// Streams matching items from cache, loading data if needed. +func (l *CloudKMSAssetLoader) SearchItems(ctx context.Context, stream discovery.QueryResultStream, scope, itemType, searchQuery string) { + // Check cache first + cacheHit, _, cachedItems, cachedErr, done := l.cache.Lookup(ctx, l.sourceName, sdp.QueryMethod_SEARCH, scope, itemType, searchQuery, false) + + if cacheHit { + done() + if cachedErr != nil { + // For SEARCH, convert NOTFOUND to empty result + if cachedErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return // Empty result is valid for SEARCH + } + stream.SendError(cachedErr) + return + } + for _, item := range cachedItems { + stream.SendItem(item) + } + return + } + + // Cache miss - trigger lazy bulk load + if err := l.EnsureLoaded(ctx); err != nil { + done() + stream.SendError(&sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: fmt.Sprintf("failed to load KMS data from Cloud Asset API: %v", err), + }) + return + } + + // Complete first lookup's pending work before second lookup to avoid self-deadlock + done() + + // Re-check cache after bulk load + cacheHit, _, cachedItems, cachedErr, done = l.cache.Lookup(ctx, l.sourceName, sdp.QueryMethod_SEARCH, scope, itemType, searchQuery, false) + defer done() + + if cacheHit { + if cachedErr != nil { + // For SEARCH, convert NOTFOUND to empty result + if cachedErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return // Empty result is valid for SEARCH + } + stream.SendError(cachedErr) + return + } + for _, item := range cachedItems { + stream.SendItem(item) + } + return + } + + // No items found for this search - return empty result +} + +// ListItems performs the cache-lookup-load-recheck pattern for LIST queries. +// Streams all items of the given type from cache, loading data if needed. +func (l *CloudKMSAssetLoader) ListItems(ctx context.Context, stream discovery.QueryResultStream, scope, itemType string) { + // Check cache first (LIST cache key has empty query) + cacheHit, _, cachedItems, cachedErr, done := l.cache.Lookup(ctx, l.sourceName, sdp.QueryMethod_LIST, scope, itemType, "", false) + + if cacheHit { + done() + if cachedErr != nil { + // For LIST, convert NOTFOUND to empty result + if cachedErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return // Empty result is valid for LIST + } + stream.SendError(cachedErr) + return + } + for _, item := range cachedItems { + stream.SendItem(item) + } + return + } + + // Cache miss - trigger lazy bulk load + if err := l.EnsureLoaded(ctx); err != nil { + done() + stream.SendError(&sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: fmt.Sprintf("failed to load KMS data from Cloud Asset API: %v", err), + }) + return + } + + // Complete first lookup's pending work before second lookup to avoid self-deadlock + done() + + // Re-check cache after bulk load + cacheHit, _, cachedItems, cachedErr, done = l.cache.Lookup(ctx, l.sourceName, sdp.QueryMethod_LIST, scope, itemType, "", false) + defer done() + + if cacheHit { + if cachedErr != nil { + // For LIST, convert NOTFOUND to empty result + if cachedErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return // Empty result is valid for LIST + } + stream.SendError(cachedErr) + return + } + for _, item := range cachedItems { + stream.SendItem(item) + } + return + } + + // No items found - return empty result +} diff --git a/sources/gcp/shared/kms-clients.go b/sources/gcp/shared/kms-clients.go deleted file mode 100644 index 5109271c..00000000 --- a/sources/gcp/shared/kms-clients.go +++ /dev/null @@ -1,123 +0,0 @@ -package shared - -//go:generate mockgen -destination=./mocks/mock_kms_clients.go -package=mocks -source=kms-clients.go - -import ( - "context" - - kms "cloud.google.com/go/kms/apiv1" - "cloud.google.com/go/kms/apiv1/kmspb" - "github.com/googleapis/gax-go/v2" - locationpb "google.golang.org/genproto/googleapis/cloud/location" -) - -// CloudKMSKeyRingIterator is an interface for iterating over KMS KeyRings -type CloudKMSKeyRingIterator interface { - Next() (*kmspb.KeyRing, error) -} - -// CloudKMSLocationIterator is an interface for iterating over KMS Locations -type CloudKMSLocationIterator interface { - Next() (*locationpb.Location, error) -} - -// CloudKMSKeyRingClient is an interface for the KMS KeyRing client -type CloudKMSKeyRingClient interface { - Get(ctx context.Context, req *kmspb.GetKeyRingRequest, opts ...gax.CallOption) (*kmspb.KeyRing, error) - Search(ctx context.Context, req *kmspb.ListKeyRingsRequest, opts ...gax.CallOption) CloudKMSKeyRingIterator - ListLocations(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) CloudKMSLocationIterator -} - -// cloudKMSKeyRingClient is a concrete implementation of CloudKMSKeyRingClient -type cloudKMSKeyRingClient struct { - client *kms.KeyManagementClient -} - -// NewCloudKMSKeyRingClient creates a new CloudKMSKeyRingClient -func NewCloudKMSKeyRingClient(keyRingClient *kms.KeyManagementClient) CloudKMSKeyRingClient { - return &cloudKMSKeyRingClient{ - client: keyRingClient, - } -} - -// Get retrieves a KMS KeyRing -func (c cloudKMSKeyRingClient) Get(ctx context.Context, req *kmspb.GetKeyRingRequest, opts ...gax.CallOption) (*kmspb.KeyRing, error) { - return c.client.GetKeyRing(ctx, req, opts...) -} - -// List lists KMS KeyRings and returns an iterator -func (c cloudKMSKeyRingClient) Search(ctx context.Context, req *kmspb.ListKeyRingsRequest, opts ...gax.CallOption) CloudKMSKeyRingIterator { - return c.client.ListKeyRings(ctx, req, opts...) -} - -// ListLocations lists KMS Locations and returns an iterator -func (c cloudKMSKeyRingClient) ListLocations(ctx context.Context, req *locationpb.ListLocationsRequest, opts ...gax.CallOption) CloudKMSLocationIterator { - return c.client.ListLocations(ctx, req, opts...) -} - -// CloudKMSCryptoKeyVersionIterator is an interface for iterating over Cloud KMS CryptoKeyVersions -type CloudKMSCryptoKeyVersionIterator interface { - Next() (*kmspb.CryptoKeyVersion, error) -} - -// CloudKMSCryptoKeyVersionClient is an interface for the Cloud KMS CryptoKeyVersion client -type CloudKMSCryptoKeyVersionClient interface { - Get(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) - List(ctx context.Context, req *kmspb.ListCryptoKeyVersionsRequest, opts ...gax.CallOption) CloudKMSCryptoKeyVersionIterator -} - -// CloudKMSCryptoKeyIterator is an interface for iterating over KMS CryptoKeys -type CloudKMSCryptoKeyIterator interface { - Next() (*kmspb.CryptoKey, error) -} - -// CloudKMSCryptoKeyClient is an interface for the KMS CryptoKey client -type CloudKMSCryptoKeyClient interface { - Get(ctx context.Context, req *kmspb.GetCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) - List(ctx context.Context, req *kmspb.ListCryptoKeysRequest, opts ...gax.CallOption) CloudKMSCryptoKeyIterator -} - -// cloudKMSCryptoKeyClient is a concrete implementation of CloudKMSCryptoKeyClient -type cloudKMSCryptoKeyClient struct { - client *kms.KeyManagementClient -} - -// NewCloudKMSCryptoKeyClient creates a new CloudKMSCryptoKeyClient -func NewCloudKMSCryptoKeyClient(cryptoKeyClient *kms.KeyManagementClient) CloudKMSCryptoKeyClient { - return &cloudKMSCryptoKeyClient{ - client: cryptoKeyClient, - } -} - -// Get retrieves a KMS CryptoKey -func (c cloudKMSCryptoKeyClient) Get(ctx context.Context, req *kmspb.GetCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) { - // Client options are ignored on individual calls - return c.client.GetCryptoKey(ctx, req, opts...) -} - -// List lists KMS CryptoKeys and returns an iterator -func (c cloudKMSCryptoKeyClient) List(ctx context.Context, req *kmspb.ListCryptoKeysRequest, opts ...gax.CallOption) CloudKMSCryptoKeyIterator { - return c.client.ListCryptoKeys(ctx, req, opts...) -} - -// cloudKMSCryptoKeyVersionClient is a concrete implementation of CloudKMSCryptoKeyVersionClient -type cloudKMSCryptoKeyVersionClient struct { - client *kms.KeyManagementClient -} - -// NewCloudKMSCryptoKeyVersionClient creates a new CloudKMSCryptoKeyVersionClient -func NewCloudKMSCryptoKeyVersionClient(client *kms.KeyManagementClient) CloudKMSCryptoKeyVersionClient { - return &cloudKMSCryptoKeyVersionClient{ - client: client, - } -} - -// Get retrieves a KMS CryptoKeyVersion -func (c cloudKMSCryptoKeyVersionClient) Get(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { - return c.client.GetCryptoKeyVersion(ctx, req, opts...) -} - -// List lists KMS CryptoKeyVersions and returns an iterator -func (c cloudKMSCryptoKeyVersionClient) List(ctx context.Context, req *kmspb.ListCryptoKeyVersionsRequest, opts ...gax.CallOption) CloudKMSCryptoKeyVersionIterator { - return c.client.ListCryptoKeyVersions(ctx, req, opts...) -} diff --git a/sources/gcp/shared/linker.go b/sources/gcp/shared/linker.go index 0bac3e8f..82c773d0 100644 --- a/sources/gcp/shared/linker.go +++ b/sources/gcp/shared/linker.go @@ -261,38 +261,6 @@ func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemV } } -func (l *Linker) tryGlobalResources(fromSDPItem *sdp.Item, toItemValue string) { //nolint: unused - if isIPAddress(toItemValue) { - fromSDPItem.LinkedItemQueries = append(fromSDPItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: "ip", - Method: sdp.QueryMethod_GET, - Query: toItemValue, - Scope: "global", - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }) - } - - if isDNSName(toItemValue) { - fromSDPItem.LinkedItemQueries = append(fromSDPItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: "dns", - Method: sdp.QueryMethod_SEARCH, - Query: toItemValue, - Scope: "global", - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }) - } -} - func isIPAddress(s string) bool { return net.ParseIP(s) != nil } diff --git a/sources/gcp/shared/manual-adapter-links.go b/sources/gcp/shared/manual-adapter-links.go index f6fc63b7..fab136b8 100644 --- a/sources/gcp/shared/manual-adapter-links.go +++ b/sources/gcp/shared/manual-adapter-links.go @@ -508,24 +508,25 @@ func AWSLinkByARN(awsItem string) func(_, _, arn string, blastPropagation *sdp.B // // 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{ - ComputeInstance: ZoneBaseLinkedItemQueryByName(ComputeInstance), - ComputeInstanceGroup: ZoneBaseLinkedItemQueryByName(ComputeInstanceGroup), - ComputeInstanceGroupManager: ZoneBaseLinkedItemQueryByName(ComputeInstanceGroupManager), - ComputeAutoscaler: ZoneBaseLinkedItemQueryByName(ComputeAutoscaler), - ComputeDisk: ZoneBaseLinkedItemQueryByName(ComputeDisk), - ComputeReservation: ZoneBaseLinkedItemQueryByName(ComputeReservation), - ComputeNodeGroup: ZoneBaseLinkedItemQueryByName(ComputeNodeGroup), - ComputeInstantSnapshot: ZoneBaseLinkedItemQueryByName(ComputeInstantSnapshot), - ComputeMachineImage: ProjectBaseLinkedItemQueryByName(ComputeMachineImage), - ComputeSecurityPolicy: ProjectBaseLinkedItemQueryByName(ComputeSecurityPolicy), - ComputeSnapshot: ProjectBaseLinkedItemQueryByName(ComputeSnapshot), - ComputeHealthCheck: HealthCheckLinker, // Handles both global and regional health checks - ComputeBackendService: BackendServiceOrBucketLinker, // Handles both global and regional backend services, plus backend buckets - ComputeImage: ComputeImageLinker, // Custom linker that uses SEARCH for all image references (handles both names and families) - ComputeAddress: RegionBaseLinkedItemQueryByName(ComputeAddress), - ComputeForwardingRule: RegionBaseLinkedItemQueryByName(ComputeForwardingRule), - ComputeInterconnectAttachment: RegionBaseLinkedItemQueryByName(ComputeInterconnectAttachment), - ComputeNodeTemplate: RegionBaseLinkedItemQueryByName(ComputeNodeTemplate), + ComputeInstance: ZoneBaseLinkedItemQueryByName(ComputeInstance), + ComputeInstanceGroup: ZoneBaseLinkedItemQueryByName(ComputeInstanceGroup), + ComputeInstanceGroupManager: ZoneBaseLinkedItemQueryByName(ComputeInstanceGroupManager), + ComputeRegionInstanceGroupManager: RegionBaseLinkedItemQueryByName(ComputeRegionInstanceGroupManager), + ComputeAutoscaler: ZoneBaseLinkedItemQueryByName(ComputeAutoscaler), + ComputeDisk: ZoneBaseLinkedItemQueryByName(ComputeDisk), + ComputeReservation: ZoneBaseLinkedItemQueryByName(ComputeReservation), + ComputeNodeGroup: ZoneBaseLinkedItemQueryByName(ComputeNodeGroup), + ComputeInstantSnapshot: ZoneBaseLinkedItemQueryByName(ComputeInstantSnapshot), + ComputeMachineImage: ProjectBaseLinkedItemQueryByName(ComputeMachineImage), + ComputeSecurityPolicy: ProjectBaseLinkedItemQueryByName(ComputeSecurityPolicy), + ComputeSnapshot: ProjectBaseLinkedItemQueryByName(ComputeSnapshot), + ComputeHealthCheck: HealthCheckLinker, // Handles both global and regional health checks + ComputeBackendService: BackendServiceOrBucketLinker, // Handles both global and regional backend services, plus backend buckets + ComputeImage: ComputeImageLinker, // Custom linker that uses SEARCH for all image references (handles both names and families) + ComputeAddress: RegionBaseLinkedItemQueryByName(ComputeAddress), + ComputeForwardingRule: RegionBaseLinkedItemQueryByName(ComputeForwardingRule), + ComputeInterconnectAttachment: RegionBaseLinkedItemQueryByName(ComputeInterconnectAttachment), + ComputeNodeTemplate: RegionBaseLinkedItemQueryByName(ComputeNodeTemplate), // Target proxy types (global, project-scoped) - use polymorphic linker for forwarding rule target field ComputeTargetHttpProxy: ForwardingRuleTargetLinker, ComputeTargetHttpsProxy: ForwardingRuleTargetLinker, diff --git a/sources/gcp/shared/mocks/mock_compute_instance_client.go b/sources/gcp/shared/mocks/mock_compute_instance_client.go index af778a26..3cbf2dfa 100644 --- a/sources/gcp/shared/mocks/mock_compute_instance_client.go +++ b/sources/gcp/shared/mocks/mock_compute_instance_client.go @@ -622,6 +622,108 @@ func (mr *MockComputeInstanceGroupManagerClientMockRecorder) List(ctx, req any, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockComputeInstanceGroupManagerClient)(nil).List), varargs...) } +// MockRegionInstanceGroupManagerIterator is a mock of RegionInstanceGroupManagerIterator interface. +type MockRegionInstanceGroupManagerIterator struct { + ctrl *gomock.Controller + recorder *MockRegionInstanceGroupManagerIteratorMockRecorder + isgomock struct{} +} + +// MockRegionInstanceGroupManagerIteratorMockRecorder is the mock recorder for MockRegionInstanceGroupManagerIterator. +type MockRegionInstanceGroupManagerIteratorMockRecorder struct { + mock *MockRegionInstanceGroupManagerIterator +} + +// NewMockRegionInstanceGroupManagerIterator creates a new mock instance. +func NewMockRegionInstanceGroupManagerIterator(ctrl *gomock.Controller) *MockRegionInstanceGroupManagerIterator { + mock := &MockRegionInstanceGroupManagerIterator{ctrl: ctrl} + mock.recorder = &MockRegionInstanceGroupManagerIteratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRegionInstanceGroupManagerIterator) EXPECT() *MockRegionInstanceGroupManagerIteratorMockRecorder { + return m.recorder +} + +// Next mocks base method. +func (m *MockRegionInstanceGroupManagerIterator) Next() (*computepb.InstanceGroupManager, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Next") + ret0, _ := ret[0].(*computepb.InstanceGroupManager) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Next indicates an expected call of Next. +func (mr *MockRegionInstanceGroupManagerIteratorMockRecorder) Next() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockRegionInstanceGroupManagerIterator)(nil).Next)) +} + +// MockRegionInstanceGroupManagerClient is a mock of RegionInstanceGroupManagerClient interface. +type MockRegionInstanceGroupManagerClient struct { + ctrl *gomock.Controller + recorder *MockRegionInstanceGroupManagerClientMockRecorder + isgomock struct{} +} + +// MockRegionInstanceGroupManagerClientMockRecorder is the mock recorder for MockRegionInstanceGroupManagerClient. +type MockRegionInstanceGroupManagerClientMockRecorder struct { + mock *MockRegionInstanceGroupManagerClient +} + +// NewMockRegionInstanceGroupManagerClient creates a new mock instance. +func NewMockRegionInstanceGroupManagerClient(ctrl *gomock.Controller) *MockRegionInstanceGroupManagerClient { + mock := &MockRegionInstanceGroupManagerClient{ctrl: ctrl} + mock.recorder = &MockRegionInstanceGroupManagerClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRegionInstanceGroupManagerClient) EXPECT() *MockRegionInstanceGroupManagerClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockRegionInstanceGroupManagerClient) Get(ctx context.Context, req *computepb.GetRegionInstanceGroupManagerRequest, opts ...gax.CallOption) (*computepb.InstanceGroupManager, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(*computepb.InstanceGroupManager) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockRegionInstanceGroupManagerClientMockRecorder) Get(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, "Get", reflect.TypeOf((*MockRegionInstanceGroupManagerClient)(nil).Get), varargs...) +} + +// List mocks base method. +func (m *MockRegionInstanceGroupManagerClient) List(ctx context.Context, req *computepb.ListRegionInstanceGroupManagersRequest, opts ...gax.CallOption) shared.RegionInstanceGroupManagerIterator { + m.ctrl.T.Helper() + varargs := []any{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(shared.RegionInstanceGroupManagerIterator) + return ret0 +} + +// List indicates an expected call of List. +func (mr *MockRegionInstanceGroupManagerClientMockRecorder) List(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, "List", reflect.TypeOf((*MockRegionInstanceGroupManagerClient)(nil).List), varargs...) +} + // MockForwardingRuleIterator is a mock of ForwardingRuleIterator interface. type MockForwardingRuleIterator struct { ctrl *gomock.Controller diff --git a/sources/gcp/shared/mocks/mock_kms_clients.go b/sources/gcp/shared/mocks/mock_kms_clients.go deleted file mode 100644 index b51198a4..00000000 --- a/sources/gcp/shared/mocks/mock_kms_clients.go +++ /dev/null @@ -1,385 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: kms-clients.go -// -// Generated by this command: -// -// mockgen -destination=./mocks/mock_kms_clients.go -package=mocks -source=kms-clients.go -// - -// Package mocks is a generated GoMock package. -package mocks - -import ( - context "context" - reflect "reflect" - - kmspb "cloud.google.com/go/kms/apiv1/kmspb" - gax "github.com/googleapis/gax-go/v2" - shared "github.com/overmindtech/cli/sources/gcp/shared" - gomock "go.uber.org/mock/gomock" - location "google.golang.org/genproto/googleapis/cloud/location" -) - -// MockCloudKMSKeyRingIterator is a mock of CloudKMSKeyRingIterator interface. -type MockCloudKMSKeyRingIterator struct { - ctrl *gomock.Controller - recorder *MockCloudKMSKeyRingIteratorMockRecorder - isgomock struct{} -} - -// MockCloudKMSKeyRingIteratorMockRecorder is the mock recorder for MockCloudKMSKeyRingIterator. -type MockCloudKMSKeyRingIteratorMockRecorder struct { - mock *MockCloudKMSKeyRingIterator -} - -// NewMockCloudKMSKeyRingIterator creates a new mock instance. -func NewMockCloudKMSKeyRingIterator(ctrl *gomock.Controller) *MockCloudKMSKeyRingIterator { - mock := &MockCloudKMSKeyRingIterator{ctrl: ctrl} - mock.recorder = &MockCloudKMSKeyRingIteratorMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCloudKMSKeyRingIterator) EXPECT() *MockCloudKMSKeyRingIteratorMockRecorder { - return m.recorder -} - -// Next mocks base method. -func (m *MockCloudKMSKeyRingIterator) Next() (*kmspb.KeyRing, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Next") - ret0, _ := ret[0].(*kmspb.KeyRing) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Next indicates an expected call of Next. -func (mr *MockCloudKMSKeyRingIteratorMockRecorder) Next() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockCloudKMSKeyRingIterator)(nil).Next)) -} - -// MockCloudKMSLocationIterator is a mock of CloudKMSLocationIterator interface. -type MockCloudKMSLocationIterator struct { - ctrl *gomock.Controller - recorder *MockCloudKMSLocationIteratorMockRecorder - isgomock struct{} -} - -// MockCloudKMSLocationIteratorMockRecorder is the mock recorder for MockCloudKMSLocationIterator. -type MockCloudKMSLocationIteratorMockRecorder struct { - mock *MockCloudKMSLocationIterator -} - -// NewMockCloudKMSLocationIterator creates a new mock instance. -func NewMockCloudKMSLocationIterator(ctrl *gomock.Controller) *MockCloudKMSLocationIterator { - mock := &MockCloudKMSLocationIterator{ctrl: ctrl} - mock.recorder = &MockCloudKMSLocationIteratorMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCloudKMSLocationIterator) EXPECT() *MockCloudKMSLocationIteratorMockRecorder { - return m.recorder -} - -// Next mocks base method. -func (m *MockCloudKMSLocationIterator) Next() (*location.Location, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Next") - ret0, _ := ret[0].(*location.Location) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Next indicates an expected call of Next. -func (mr *MockCloudKMSLocationIteratorMockRecorder) Next() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockCloudKMSLocationIterator)(nil).Next)) -} - -// MockCloudKMSKeyRingClient is a mock of CloudKMSKeyRingClient interface. -type MockCloudKMSKeyRingClient struct { - ctrl *gomock.Controller - recorder *MockCloudKMSKeyRingClientMockRecorder - isgomock struct{} -} - -// MockCloudKMSKeyRingClientMockRecorder is the mock recorder for MockCloudKMSKeyRingClient. -type MockCloudKMSKeyRingClientMockRecorder struct { - mock *MockCloudKMSKeyRingClient -} - -// NewMockCloudKMSKeyRingClient creates a new mock instance. -func NewMockCloudKMSKeyRingClient(ctrl *gomock.Controller) *MockCloudKMSKeyRingClient { - mock := &MockCloudKMSKeyRingClient{ctrl: ctrl} - mock.recorder = &MockCloudKMSKeyRingClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCloudKMSKeyRingClient) EXPECT() *MockCloudKMSKeyRingClientMockRecorder { - return m.recorder -} - -// Get mocks base method. -func (m *MockCloudKMSKeyRingClient) Get(ctx context.Context, req *kmspb.GetKeyRingRequest, opts ...gax.CallOption) (*kmspb.KeyRing, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, req} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Get", varargs...) - ret0, _ := ret[0].(*kmspb.KeyRing) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get. -func (mr *MockCloudKMSKeyRingClientMockRecorder) Get(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, "Get", reflect.TypeOf((*MockCloudKMSKeyRingClient)(nil).Get), varargs...) -} - -// ListLocations mocks base method. -func (m *MockCloudKMSKeyRingClient) ListLocations(ctx context.Context, req *location.ListLocationsRequest, opts ...gax.CallOption) shared.CloudKMSLocationIterator { - m.ctrl.T.Helper() - varargs := []any{ctx, req} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "ListLocations", varargs...) - ret0, _ := ret[0].(shared.CloudKMSLocationIterator) - return ret0 -} - -// ListLocations indicates an expected call of ListLocations. -func (mr *MockCloudKMSKeyRingClientMockRecorder) ListLocations(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, "ListLocations", reflect.TypeOf((*MockCloudKMSKeyRingClient)(nil).ListLocations), varargs...) -} - -// Search mocks base method. -func (m *MockCloudKMSKeyRingClient) Search(ctx context.Context, req *kmspb.ListKeyRingsRequest, opts ...gax.CallOption) shared.CloudKMSKeyRingIterator { - m.ctrl.T.Helper() - varargs := []any{ctx, req} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Search", varargs...) - ret0, _ := ret[0].(shared.CloudKMSKeyRingIterator) - return ret0 -} - -// Search indicates an expected call of Search. -func (mr *MockCloudKMSKeyRingClientMockRecorder) Search(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, "Search", reflect.TypeOf((*MockCloudKMSKeyRingClient)(nil).Search), varargs...) -} - -// MockCloudKMSCryptoKeyVersionIterator is a mock of CloudKMSCryptoKeyVersionIterator interface. -type MockCloudKMSCryptoKeyVersionIterator struct { - ctrl *gomock.Controller - recorder *MockCloudKMSCryptoKeyVersionIteratorMockRecorder - isgomock struct{} -} - -// MockCloudKMSCryptoKeyVersionIteratorMockRecorder is the mock recorder for MockCloudKMSCryptoKeyVersionIterator. -type MockCloudKMSCryptoKeyVersionIteratorMockRecorder struct { - mock *MockCloudKMSCryptoKeyVersionIterator -} - -// NewMockCloudKMSCryptoKeyVersionIterator creates a new mock instance. -func NewMockCloudKMSCryptoKeyVersionIterator(ctrl *gomock.Controller) *MockCloudKMSCryptoKeyVersionIterator { - mock := &MockCloudKMSCryptoKeyVersionIterator{ctrl: ctrl} - mock.recorder = &MockCloudKMSCryptoKeyVersionIteratorMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCloudKMSCryptoKeyVersionIterator) EXPECT() *MockCloudKMSCryptoKeyVersionIteratorMockRecorder { - return m.recorder -} - -// Next mocks base method. -func (m *MockCloudKMSCryptoKeyVersionIterator) Next() (*kmspb.CryptoKeyVersion, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Next") - ret0, _ := ret[0].(*kmspb.CryptoKeyVersion) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Next indicates an expected call of Next. -func (mr *MockCloudKMSCryptoKeyVersionIteratorMockRecorder) Next() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockCloudKMSCryptoKeyVersionIterator)(nil).Next)) -} - -// MockCloudKMSCryptoKeyVersionClient is a mock of CloudKMSCryptoKeyVersionClient interface. -type MockCloudKMSCryptoKeyVersionClient struct { - ctrl *gomock.Controller - recorder *MockCloudKMSCryptoKeyVersionClientMockRecorder - isgomock struct{} -} - -// MockCloudKMSCryptoKeyVersionClientMockRecorder is the mock recorder for MockCloudKMSCryptoKeyVersionClient. -type MockCloudKMSCryptoKeyVersionClientMockRecorder struct { - mock *MockCloudKMSCryptoKeyVersionClient -} - -// NewMockCloudKMSCryptoKeyVersionClient creates a new mock instance. -func NewMockCloudKMSCryptoKeyVersionClient(ctrl *gomock.Controller) *MockCloudKMSCryptoKeyVersionClient { - mock := &MockCloudKMSCryptoKeyVersionClient{ctrl: ctrl} - mock.recorder = &MockCloudKMSCryptoKeyVersionClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCloudKMSCryptoKeyVersionClient) EXPECT() *MockCloudKMSCryptoKeyVersionClientMockRecorder { - return m.recorder -} - -// Get mocks base method. -func (m *MockCloudKMSCryptoKeyVersionClient) Get(ctx context.Context, req *kmspb.GetCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, req} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Get", varargs...) - ret0, _ := ret[0].(*kmspb.CryptoKeyVersion) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get. -func (mr *MockCloudKMSCryptoKeyVersionClientMockRecorder) Get(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, "Get", reflect.TypeOf((*MockCloudKMSCryptoKeyVersionClient)(nil).Get), varargs...) -} - -// List mocks base method. -func (m *MockCloudKMSCryptoKeyVersionClient) List(ctx context.Context, req *kmspb.ListCryptoKeyVersionsRequest, opts ...gax.CallOption) shared.CloudKMSCryptoKeyVersionIterator { - m.ctrl.T.Helper() - varargs := []any{ctx, req} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "List", varargs...) - ret0, _ := ret[0].(shared.CloudKMSCryptoKeyVersionIterator) - return ret0 -} - -// List indicates an expected call of List. -func (mr *MockCloudKMSCryptoKeyVersionClientMockRecorder) List(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, "List", reflect.TypeOf((*MockCloudKMSCryptoKeyVersionClient)(nil).List), varargs...) -} - -// MockCloudKMSCryptoKeyIterator is a mock of CloudKMSCryptoKeyIterator interface. -type MockCloudKMSCryptoKeyIterator struct { - ctrl *gomock.Controller - recorder *MockCloudKMSCryptoKeyIteratorMockRecorder - isgomock struct{} -} - -// MockCloudKMSCryptoKeyIteratorMockRecorder is the mock recorder for MockCloudKMSCryptoKeyIterator. -type MockCloudKMSCryptoKeyIteratorMockRecorder struct { - mock *MockCloudKMSCryptoKeyIterator -} - -// NewMockCloudKMSCryptoKeyIterator creates a new mock instance. -func NewMockCloudKMSCryptoKeyIterator(ctrl *gomock.Controller) *MockCloudKMSCryptoKeyIterator { - mock := &MockCloudKMSCryptoKeyIterator{ctrl: ctrl} - mock.recorder = &MockCloudKMSCryptoKeyIteratorMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCloudKMSCryptoKeyIterator) EXPECT() *MockCloudKMSCryptoKeyIteratorMockRecorder { - return m.recorder -} - -// Next mocks base method. -func (m *MockCloudKMSCryptoKeyIterator) Next() (*kmspb.CryptoKey, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Next") - ret0, _ := ret[0].(*kmspb.CryptoKey) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Next indicates an expected call of Next. -func (mr *MockCloudKMSCryptoKeyIteratorMockRecorder) Next() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockCloudKMSCryptoKeyIterator)(nil).Next)) -} - -// MockCloudKMSCryptoKeyClient is a mock of CloudKMSCryptoKeyClient interface. -type MockCloudKMSCryptoKeyClient struct { - ctrl *gomock.Controller - recorder *MockCloudKMSCryptoKeyClientMockRecorder - isgomock struct{} -} - -// MockCloudKMSCryptoKeyClientMockRecorder is the mock recorder for MockCloudKMSCryptoKeyClient. -type MockCloudKMSCryptoKeyClientMockRecorder struct { - mock *MockCloudKMSCryptoKeyClient -} - -// NewMockCloudKMSCryptoKeyClient creates a new mock instance. -func NewMockCloudKMSCryptoKeyClient(ctrl *gomock.Controller) *MockCloudKMSCryptoKeyClient { - mock := &MockCloudKMSCryptoKeyClient{ctrl: ctrl} - mock.recorder = &MockCloudKMSCryptoKeyClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCloudKMSCryptoKeyClient) EXPECT() *MockCloudKMSCryptoKeyClientMockRecorder { - return m.recorder -} - -// Get mocks base method. -func (m *MockCloudKMSCryptoKeyClient) Get(ctx context.Context, req *kmspb.GetCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, req} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Get", varargs...) - ret0, _ := ret[0].(*kmspb.CryptoKey) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get. -func (mr *MockCloudKMSCryptoKeyClientMockRecorder) Get(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, "Get", reflect.TypeOf((*MockCloudKMSCryptoKeyClient)(nil).Get), varargs...) -} - -// List mocks base method. -func (m *MockCloudKMSCryptoKeyClient) List(ctx context.Context, req *kmspb.ListCryptoKeysRequest, opts ...gax.CallOption) shared.CloudKMSCryptoKeyIterator { - m.ctrl.T.Helper() - varargs := []any{ctx, req} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "List", varargs...) - ret0, _ := ret[0].(shared.CloudKMSCryptoKeyIterator) - return ret0 -} - -// List indicates an expected call of List. -func (mr *MockCloudKMSCryptoKeyClientMockRecorder) List(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, "List", reflect.TypeOf((*MockCloudKMSCryptoKeyClient)(nil).List), varargs...) -} diff --git a/sources/gcp/shared/models.go b/sources/gcp/shared/models.go index 2aa3ef56..b897d26e 100644 --- a/sources/gcp/shared/models.go +++ b/sources/gcp/shared/models.go @@ -70,6 +70,7 @@ const ( UrlMap shared.Resource = "url-map" Autoscaler shared.Resource = "autoscaler" InstanceGroupManager shared.Resource = "instance-group-manager" + RegionalInstanceGroupManager shared.Resource = "regional-instance-group-manager" SecurityPolicy shared.Resource = "security-policy" ClientTlsPolicy shared.Resource = "client-tls-policy" ServiceLbPolicy shared.Resource = "service-lb-policy" diff --git a/sources/gcp/shared/predefined-roles.go b/sources/gcp/shared/predefined-roles.go index 40f7ec01..e1778af1 100644 --- a/sources/gcp/shared/predefined-roles.go +++ b/sources/gcp/shared/predefined-roles.go @@ -164,6 +164,8 @@ var PredefinedRoles = map[string]role{ "compute.regionBackendServices.list", "compute.regionHealthChecks.get", "compute.regionHealthChecks.list", + "compute.regionInstanceGroupManagers.get", + "compute.regionInstanceGroupManagers.list", "compute.reservations.get", "compute.reservations.list", "compute.resourcePolicies.get", @@ -478,4 +480,12 @@ var PredefinedRoles = map[string]role{ "cloudkms.locations.list", }, }, + "roles/cloudasset.viewer": { + Role: "roles/cloudasset.viewer", + // Read-only access to Cloud Asset Inventory. + Link: "https://cloud.google.com/iam/docs/roles-permissions/cloudasset#cloudasset.viewer", + IAMPermissions: []string{ + "cloudasset.assets.listResource", + }, + }, } diff --git a/sources/transformer.go b/sources/transformer.go index 30a8d579..cef07012 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -92,8 +92,8 @@ type StandardAdapter interface { // WrapperToAdapter converts a Wrapper to a StandardAdapter. func WrapperToAdapter(wrapper Wrapper, cache sdpcache.Cache) StandardAdapter { core := standardAdapterCore{ - wrapper: wrapper, - cacheField: cache, + wrapper: wrapper, + cache: cache, } core.sourceType = "unknown" @@ -190,7 +190,7 @@ func WrapperToAdapter(wrapper Wrapper, cache sdpcache.Cache) StandardAdapter { type standardAdapterCore struct { wrapper Wrapper sourceType string - cacheField sdpcache.Cache + cache sdpcache.Cache // This is mandatory } type standardAdapterImpl struct { @@ -225,13 +225,13 @@ var ( // Cache returns the cache of the adapter. func (s *standardAdapterCore) Cache() sdpcache.Cache { - if s.cacheField == nil { + if s.cache == nil { noOpCacheTransformerOnce.Do(func() { noOpCacheTransformer = sdpcache.NewNoOpCache() }) return noOpCacheTransformer } - return s.cacheField + return s.cache } // Type returns the type of the adapter. @@ -358,7 +358,7 @@ func (s *standardAdapterImpl) Metadata() *sdp.AdapterMetadata { // Validate checks if the adapter is valid. func (s *standardAdapterImpl) Validate() error { - if s.cacheField == nil { + if s.cache == nil { return fmt.Errorf("cache is not initialized") } @@ -463,7 +463,7 @@ func (s *standardListableAdapterImpl) ListStream(ctx context.Context, scope stri return } - s.listStreamable.ListStream(ctx, stream, s.cacheField, ck, scope) + s.listStreamable.ListStream(ctx, stream, s.cache, ck, scope) } // Metadata returns the metadata of the listable adapter. @@ -502,7 +502,7 @@ func (s *standardListableAdapterImpl) Metadata() *sdp.AdapterMetadata { // Validate checks if the listable adapter is valid. func (s *standardListableAdapterImpl) Validate() error { - if s.cacheField == nil { + if s.cache == nil { return fmt.Errorf("cache is not initialized") } @@ -782,7 +782,7 @@ func (s *standardSearchableAdapterImpl) SearchStream(ctx context.Context, scope return } - s.searchStreamable.SearchStream(ctx, stream, s.cacheField, ck, scope, queryParts...) + s.searchStreamable.SearchStream(ctx, stream, s.cache, ck, scope, queryParts...) } // Metadata returns the metadata of the searchable adapter. @@ -823,7 +823,7 @@ func (s *standardSearchableAdapterImpl) Metadata() *sdp.AdapterMetadata { // Validate checks if the searchable adapter is valid. func (s *standardSearchableAdapterImpl) Validate() error { - if s.cacheField == nil { + if s.cache == nil { return fmt.Errorf("cache is not initialized") } if s.sourceType == string(gcpshared.GCP) { @@ -882,7 +882,7 @@ func (s *standardSearchableListableAdapterImpl) Metadata() *sdp.AdapterMetadata // Validate checks if the searchable+listable adapter is valid. func (s *standardSearchableListableAdapterImpl) Validate() error { - if s.cacheField == nil { + if s.cache == nil { return fmt.Errorf("cache is not initialized") } if s.sourceType == string(gcpshared.GCP) { diff --git a/stdlib-source/adapters/dns.go b/stdlib-source/adapters/dns.go index aea8a900..eddab2b5 100644 --- a/stdlib-source/adapters/dns.go +++ b/stdlib-source/adapters/dns.go @@ -29,7 +29,7 @@ type DNSAdapter struct { client dns.Client - cacheField sdpcache.Cache // The cache for this adapter (set during creation, can be nil for tests) + cache sdpcache.Cache // This is mandatory } const dnsCacheDuration = 5 * time.Minute @@ -40,13 +40,13 @@ var ( ) func (d *DNSAdapter) Cache() sdpcache.Cache { - if d.cacheField == nil { + if d.cache == nil { noOpCacheDNSOnce.Do(func() { noOpCacheDNS = sdpcache.NewNoOpCache() }) return noOpCacheDNS } - return d.cacheField + return d.cache } var DefaultServers = []string{ diff --git a/stdlib-source/adapters/http.go b/stdlib-source/adapters/http.go index 0300157f..3ad07bdc 100644 --- a/stdlib-source/adapters/http.go +++ b/stdlib-source/adapters/http.go @@ -75,7 +75,7 @@ func validateHostname(ctx context.Context, hostname string) error { } type HTTPAdapter struct { - cacheField sdpcache.Cache // The cache for this adapter (set during creation, can be nil for tests) + cache sdpcache.Cache // This is mandatory } const httpCacheDuration = 5 * time.Minute @@ -86,13 +86,13 @@ var ( ) func (s *HTTPAdapter) Cache() sdpcache.Cache { - if s.cacheField == nil { + if s.cache == nil { noOpCacheHTTPOnce.Do(func() { noOpCacheHTTP = sdpcache.NewNoOpCache() }) return noOpCacheHTTP } - return s.cacheField + return s.cache } // Type The type of items that this adapter is capable of finding diff --git a/stdlib-source/adapters/main.go b/stdlib-source/adapters/main.go index 1277e8f3..eed2da43 100644 --- a/stdlib-source/adapters/main.go +++ b/stdlib-source/adapters/main.go @@ -31,7 +31,6 @@ func InitializeEngine(ctx context.Context, ec *discovery.EngineConfig, reverseDN }).Fatal("Error initializing Engine") } - // Create a shared cache for all adapters in this source sharedCache := sdpcache.NewCache(ctx) @@ -40,10 +39,10 @@ func InitializeEngine(ctx context.Context, ec *discovery.EngineConfig, reverseDN &CertificateAdapter{}, &DNSAdapter{ ReverseLookup: reverseDNS, - cacheField: sharedCache, + cache: sharedCache, }, &HTTPAdapter{ - cacheField: sharedCache, + cache: sharedCache, }, &IPAdapter{}, &test.TestDogAdapter{}, diff --git a/stdlib-source/cmd/root.go b/stdlib-source/cmd/root.go index 18e6543e..8b92e503 100644 --- a/stdlib-source/cmd/root.go +++ b/stdlib-source/cmd/root.go @@ -3,12 +3,11 @@ package cmd import ( "context" "fmt" - "net/http" "os" "os/signal" + "strconv" "strings" "syscall" - "time" "github.com/getsentry/sentry-go" "github.com/overmindtech/cli/discovery" @@ -68,6 +67,10 @@ var rootCmd = &cobra.Command{ // Start HTTP server for health checks healthCheckPort := viper.GetString("service-port") + healthCheckPortInt, err := strconv.Atoi(healthCheckPort) + if err != nil { + log.WithError(err).WithFields(log.Fields{"service-port": healthCheckPort}).Fatal("Invalid service-port") + } healthCheckDNSAdapter := adapters.DNSAdapter{} @@ -86,34 +89,7 @@ var rootCmd = &cobra.Command{ return nil }) - // Liveness: Check only engine initialization (NATS, heartbeats) - http.HandleFunc("/healthz/alive", e.LivenessProbeHandlerFunc()) - // Readiness: Check if adapters are healthy and ready to handle requests - http.HandleFunc("/healthz/ready", e.ReadinessProbeHandlerFunc()) - // Backward compatibility - maps to liveness check (matches old behavior) - http.HandleFunc("/healthz", e.LivenessProbeHandlerFunc()) - - log.WithFields(log.Fields{ - "port": healthCheckPort, - }).Debug("Starting healthcheck server with endpoints: /healthz/alive, /healthz/ready, /healthz") - - go func() { - defer sentry.Recover() - - server := &http.Server{ - Addr: fmt.Sprintf(":%v", healthCheckPort), - Handler: nil, - // due to https://github.com/securego/gosec/pull/842 - ReadTimeout: 5 * time.Second, // Set the read timeout to 5 seconds - WriteTimeout: 5 * time.Second, // Set the write timeout to 5 seconds - } - - err := server.ListenAndServe() - - log.WithError(err).WithFields(log.Fields{ - "port": healthCheckPort, - }).Error("Could not start HTTP server for health checks") - }() + e.ServeHealthProbes(healthCheckPortInt) err = e.Start(ctx) if err != nil { diff --git a/tfutils/testdata/providers.tf b/tfutils/testdata/providers.tf index f25902cf..05657d17 100644 --- a/tfutils/testdata/providers.tf +++ b/tfutils/testdata/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.60" + version = "~> 6.28" } }